use crate::context::apply_context_effects;
use crate::entity::Entity;
use crate::enums::{AlertTrigger, Direction, Emotion, HexacoPath, LifeDomain, LifeStage, StatePath};
use crate::memory::{
apply_memory_consolidation, create_memory_from_event, MemoryEntry, MemoryLayer, MemoryLayers,
};
use crate::processor::{
advance_state, apply_developmental_effects, apply_interpreted_event_to_state,
compute_trust_modulation_factor, interpret_event, regress_state,
reverse_interpreted_event_from_state, InterpretedEvent,
};
use crate::relationship::{TrustContext, TrustDecision};
use crate::simulation::{RegressionQuality, Simulation, TimestampedEvent};
use crate::state::{
apply_formative_modifiers, effective_base_at, BaseShiftRecord, IndividualState,
StateInterpreter,
};
use crate::types::{Alert, Duration, EntityId, Timestamp};
use std::collections::HashMap;
pub struct EntityQueryHandle<'a> {
simulation: &'a Simulation,
entity_id: EntityId,
}
impl<'a> EntityQueryHandle<'a> {
pub(crate) fn new(simulation: &'a Simulation, entity_id: EntityId) -> Self {
EntityQueryHandle {
simulation,
entity_id,
}
}
#[must_use]
pub fn entity_id(&self) -> &EntityId {
&self.entity_id
}
#[must_use]
pub fn anchor_timestamp(&self) -> Option<Timestamp> {
self.simulation
.get_anchored_entity(&self.entity_id)
.map(|a| a.anchor_timestamp())
}
#[must_use]
pub fn state_at(&self, timestamp: Timestamp) -> ComputedState {
let anchored = self
.simulation
.get_anchored_entity(&self.entity_id)
.expect("EntityQueryHandle created for non-existent entity - use Simulation::entity() to check existence");
let anchor_timestamp = anchored.anchor_timestamp();
let entity = anchored.entity();
let mut state = entity.individual_state().clone();
let species = entity.species().clone();
if timestamp == anchor_timestamp {
let age_at_timestamp = self.compute_age_at_timestamp(entity, timestamp);
let life_stage =
LifeStage::from_age_years_for_species(&species, age_at_timestamp.as_years() as f64);
let pad_bounds = entity.context().compute_pad_bounds();
state
.mood_mut()
.valence_mut()
.set_bounds(pad_bounds.valence_min, pad_bounds.valence_max);
state
.mood_mut()
.arousal_mut()
.set_bounds(pad_bounds.arousal_min, pad_bounds.arousal_max);
state
.mood_mut()
.dominance_mut()
.set_bounds(pad_bounds.dominance_min, pad_bounds.dominance_max);
let interpreter = StateInterpreter::from_state(&state);
return ComputedState {
individual_state: state,
age_at_timestamp,
life_stage,
regression_quality: RegressionQuality::Exact,
alerts: std::cell::OnceCell::new(),
interpretations: interpreter.interpretations().clone(),
summary: interpreter.summary().to_string(),
delta_summary: None,
};
}
let is_forward = timestamp > anchor_timestamp;
let events = self.get_sorted_events_for_range(anchor_timestamp, timestamp, is_forward);
let regression_quality = if is_forward {
RegressionQuality::Exact
} else {
self.determine_regression_quality(&events)
};
let interpreted_events: Vec<InterpretedEvent> = events
.iter()
.map(|te| interpret_event(te.event(), entity))
.collect();
let base_shift_records: Vec<BaseShiftRecord> =
collect_base_shift_records(&events, entity, timestamp, is_forward);
let compute_age_at = |ts: Timestamp| -> Duration {
if let Some(birth_date) = entity.birth_date() {
if ts >= birth_date {
ts - birth_date
} else {
Duration::zero()
}
} else {
entity.age()
}
};
let mut computed_memories = MemoryLayers::new();
if is_forward {
let mut cursor = anchor_timestamp;
for (te, interpreted) in events.iter().zip(interpreted_events.iter()) {
let delta = te.timestamp() - cursor;
state = advance_state(state, delta);
let age_at_event = compute_age_at(te.timestamp());
let age_days = age_at_event.as_days();
let dev_factor =
apply_developmental_effects(entity, te.event(), 1.0, age_days, te.timestamp());
let trustor_propensity = state.disposition().trustor_propensity_effective();
let trust_factor = self.compute_trust_factor_for_event(
entity,
te.event(),
te.timestamp(),
trustor_propensity,
);
let combined_factor = dev_factor * trust_factor;
let scaled_interpreted = interpreted.scaled_by(combined_factor);
state = apply_interpreted_event_to_state(state, &scaled_interpreted);
let memory = create_memory_from_event(
&scaled_interpreted,
&state,
age_at_event,
Some(&self.entity_id),
);
computed_memories.add(MemoryLayer::Immediate, memory);
cursor = te.timestamp();
}
let remaining = timestamp - cursor;
state = advance_state(state, remaining);
} else {
let mut cursor = anchor_timestamp;
for i in (0..events.len()).rev() {
let te = &events[i];
let interpreted = &interpreted_events[i];
let delta = cursor - te.timestamp();
state = regress_state(state, delta);
let age_at_event = compute_age_at(te.timestamp());
let age_days = age_at_event.as_days();
let dev_factor =
apply_developmental_effects(entity, te.event(), 1.0, age_days, te.timestamp());
let trustor_propensity = state.disposition().trustor_propensity_effective();
let trust_factor = self.compute_trust_factor_for_event(
entity,
te.event(),
te.timestamp(),
trustor_propensity,
);
let combined_factor = dev_factor * trust_factor;
let scaled_interpreted = interpreted.scaled_by(combined_factor);
state = reverse_interpreted_event_from_state(state, &scaled_interpreted);
cursor = te.timestamp();
}
let remaining = cursor - timestamp;
state = regress_state(state, remaining);
}
let total_duration = if is_forward {
timestamp - anchor_timestamp
} else {
anchor_timestamp - timestamp
};
let relationship_quality = estimate_relationship_quality(entity);
let age_at_timestamp = self.compute_age_at_timestamp(entity, timestamp);
let life_stage =
LifeStage::from_age_years_for_species(&species, age_at_timestamp.as_years() as f64);
state = apply_context_effects(
state,
entity.context(),
relationship_quality,
total_duration,
life_stage,
age_at_timestamp.as_years() as f64,
timestamp,
);
let merged_memories = if is_forward {
merge_memory_layers(entity.memories(), &computed_memories)
} else {
entity.memories().clone()
};
state = apply_memory_consolidation(state, &merged_memories, total_duration);
state = apply_base_shifts_to_state(state, &base_shift_records, timestamp);
let pad_bounds = entity.context().compute_pad_bounds();
state
.mood_mut()
.valence_mut()
.set_bounds(pad_bounds.valence_min, pad_bounds.valence_max);
state
.mood_mut()
.arousal_mut()
.set_bounds(pad_bounds.arousal_min, pad_bounds.arousal_max);
state
.mood_mut()
.dominance_mut()
.set_bounds(pad_bounds.dominance_min, pad_bounds.dominance_max);
let baseline_state = entity.individual_state();
let interpreter = StateInterpreter::from_state_with_baseline(&state, baseline_state);
ComputedState {
individual_state: state,
age_at_timestamp,
life_stage,
regression_quality,
alerts: std::cell::OnceCell::new(),
interpretations: interpreter.interpretations().clone(),
summary: interpreter.summary().to_string(),
delta_summary: interpreter.delta_summary().map(|s| s.to_string()),
}
}
fn get_sorted_events_for_range(
&self,
anchor: Timestamp,
target: Timestamp,
is_forward: bool,
) -> Vec<&'a TimestampedEvent> {
let mut events: Vec<_> = self
.simulation
.events_for(&self.entity_id)
.into_iter()
.filter(|te| {
let ts = te.timestamp();
if is_forward {
ts > anchor && ts <= target
} else {
ts > target && ts <= anchor
}
})
.collect();
events.sort_by_key(|te| te.timestamp());
events
}
fn determine_regression_quality(&self, events: &[&TimestampedEvent]) -> RegressionQuality {
for te in events {
let event = te.event();
let spec = event.spec();
if spec.impact.acquired_capability > 0.0 {
return RegressionQuality::Approximate;
}
}
RegressionQuality::Exact
}
fn compute_age_at_timestamp(
&self,
entity: &crate::entity::Entity,
timestamp: Timestamp,
) -> Duration {
if let Some(birth_date) = entity.birth_date() {
if timestamp >= birth_date {
return timestamp - birth_date;
} else {
return Duration::zero();
}
}
entity.age()
}
fn compute_trust_factor_for_event(
&self,
entity: &Entity,
event: &crate::event::Event,
event_timestamp: Timestamp,
trustor_propensity: f32,
) -> f64 {
let Some(source_id) = event.source() else {
return 1.0;
};
let relationships = self.simulation.relationships_for(&self.entity_id);
let relevant_rel = relationships.iter().find(|tr| {
tr.formed_timestamp() <= event_timestamp && tr.involves(source_id)
});
let Some(timestamped_rel) = relevant_rel else {
return 1.0;
};
let direction = if timestamped_rel.entity_a() == &self.entity_id {
Direction::AToB
} else {
Direction::BToA
};
let context = self.build_trust_context(entity, event);
let life_domain = event
.payload()
.life_domain()
.unwrap_or(LifeDomain::Relationship);
compute_trust_modulation_factor(
event,
Some(timestamped_rel.relationship()),
direction,
trustor_propensity,
&context,
life_domain,
)
}
fn build_trust_context(&self, entity: &Entity, event: &crate::event::Event) -> TrustContext {
let ecology = entity.context();
let macrosystem = ecology.macrosystem();
let exosystem = ecology.exosystem();
let historical = ecology.chronosystem().historical_period();
let mut social_norms = 1.0 - macrosystem.cultural_stress;
let institutional_safeguards = macrosystem.institutional_structure.rule_of_law
* (1.0 - macrosystem.institutional_structure.corruption_level);
let institutional_support = exosystem.institutional_support;
let cultural_expectations = (historical.institutional_trust * 0.6
+ (1.0 - macrosystem.cultural_stress) * 0.4)
.clamp(0.0, 1.0);
let mut time_pressure = 0.5;
if let Some(context_id) = event.microsystem_context() {
if let Some(micro) = ecology.get_microsystem(context_id) {
let warmth = micro.warmth();
let hostility = micro.hostility();
social_norms += (warmth - hostility) * 0.2;
time_pressure = ((micro.interaction_frequency() + micro.interaction_complexity())
/ 2.0)
.clamp(0.0, 1.0);
}
}
TrustContext::new()
.with_social_norms(social_norms.clamp(0.0, 1.0) as f32)
.with_institutional_safeguards(institutional_safeguards.clamp(0.0, 1.0) as f32)
.with_time_pressure(time_pressure as f32)
.with_institutional_support(institutional_support.clamp(0.0, 1.0) as f32)
.with_cultural_expectations(cultural_expectations as f32)
}
#[must_use]
pub fn trust_decision_for(
&self,
trustee: &EntityId,
timestamp: Timestamp,
context: &TrustContext,
life_domain: LifeDomain,
stakes: Option<crate::enums::ActionStakes>,
) -> Option<TrustDecision> {
let relationships = self.simulation.relationships_for(&self.entity_id);
let relevant = relationships
.iter()
.find(|tr| tr.formed_timestamp() <= timestamp && tr.involves(trustee))?;
let direction = if relevant.entity_a() == &self.entity_id {
Direction::AToB
} else {
Direction::BToA
};
let trustor_propensity = self
.state_at(timestamp)
.individual_state()
.disposition()
.trustor_propensity_effective();
Some(
relevant
.relationship()
.compute_trust_decision(
direction,
trustor_propensity,
context,
life_domain,
stakes,
),
)
}
#[must_use]
pub fn memories_at(&self, timestamp: Timestamp) -> Vec<MemoryEntry> {
let Some(anchored) = self.simulation.get_anchored_entity(&self.entity_id) else {
return Vec::new();
};
let entity = anchored.entity();
let anchor_timestamp = anchored.anchor_timestamp();
let anchor_age = entity.age();
let age_at_timestamp = if timestamp >= anchor_timestamp {
let elapsed = timestamp - anchor_timestamp;
anchor_age + elapsed
} else {
let elapsed = anchor_timestamp - timestamp;
if elapsed < anchor_age {
anchor_age - elapsed
} else {
Duration::zero()
}
};
entity
.memories()
.all_memories()
.filter(|memory: &&MemoryEntry| memory.timestamp() <= age_at_timestamp)
.cloned()
.collect()
}
}
fn estimate_relationship_quality(entity: &Entity) -> f64 {
let attached_count = entity
.relationship_slots()
.iter()
.filter(|slot| slot.is_attached())
.count();
if attached_count > 0 {
0.5 + 0.1 * (attached_count as f64).min(5.0)
} else {
0.3
}
}
fn merge_memory_layers(anchor: &MemoryLayers, computed: &MemoryLayers) -> MemoryLayers {
let mut merged = MemoryLayers::new();
for memory in anchor.immediate() {
merged.add(MemoryLayer::Immediate, memory.clone());
}
for memory in anchor.short_term() {
merged.add(MemoryLayer::ShortTerm, memory.clone());
}
for memory in anchor.long_term() {
merged.add(MemoryLayer::LongTerm, memory.clone());
}
for memory in anchor.legacy() {
merged.add(MemoryLayer::Legacy, memory.clone());
}
for memory in computed.immediate() {
merged.add(MemoryLayer::Immediate, memory.clone());
}
for memory in computed.short_term() {
merged.add(MemoryLayer::ShortTerm, memory.clone());
}
for memory in computed.long_term() {
merged.add(MemoryLayer::LongTerm, memory.clone());
}
for memory in computed.legacy() {
merged.add(MemoryLayer::Legacy, memory.clone());
}
merged
}
#[derive(Debug)]
pub struct ComputedState {
pub individual_state: IndividualState,
pub age_at_timestamp: Duration,
pub life_stage: LifeStage,
regression_quality: RegressionQuality,
alerts: std::cell::OnceCell<Vec<Alert>>,
pub interpretations: HashMap<String, String>,
pub summary: String,
pub delta_summary: Option<String>,
}
impl ComputedState {
#[must_use]
pub fn individual_state(&self) -> &IndividualState {
&self.individual_state
}
#[must_use]
pub fn emotion_membership(&self) -> HashMap<Emotion, f64> {
self.individual_state.mood().emotion_membership()
}
#[must_use]
pub fn age_at_timestamp(&self) -> Duration {
self.age_at_timestamp
}
#[must_use]
pub fn life_stage(&self) -> LifeStage {
self.life_stage
}
#[must_use]
pub fn regression_quality(&self) -> RegressionQuality {
self.regression_quality
}
#[must_use]
pub fn alerts(&self) -> Vec<Alert> {
self.alerts.get_or_init(|| self.compute_alerts()).clone()
}
fn compute_alerts(&self) -> Vec<Alert> {
let state = &self.individual_state;
let social = state.social_cognition();
let mental = state.mental_health();
let timestamp = self.age_at_timestamp;
let mut alerts = Vec::new();
let desire = mental.compute_suicidal_desire(social);
if desire >= 0.8 {
alerts.push(Alert::critical(
AlertTrigger::threshold(
StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire),
desire as f64,
),
timestamp,
"High suicidal desire detected",
));
} else if desire >= 0.5 {
alerts.push(Alert::warning(
AlertTrigger::threshold(
StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire),
desire as f64,
),
timestamp,
"Elevated suicidal desire detected",
));
}
let attempt_risk = mental.compute_attempt_risk(social);
if attempt_risk >= 0.7 {
alerts.push(Alert::critical(
AlertTrigger::threshold(
StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk),
attempt_risk as f64,
),
timestamp,
"High attempt risk detected",
));
} else if attempt_risk >= 0.4 {
alerts.push(Alert::warning(
AlertTrigger::threshold(
StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk),
attempt_risk as f64,
),
timestamp,
"Elevated attempt risk detected",
));
}
let stress = state.needs().stress_effective();
let fatigue = state.needs().fatigue_effective();
let impulse_control = state.disposition().impulse_control_effective();
if stress > 0.8 && fatigue > 0.7 && impulse_control < 0.3 {
alerts.push(Alert::warning(
AlertTrigger::spiral(crate::enums::SpiralType::Stress),
timestamp,
"Stress spiral indicators detected",
));
}
let depression = mental.depression_effective();
let loneliness = social.loneliness_effective();
if depression > 0.7 && loneliness > 0.7 {
alerts.push(Alert::warning(
AlertTrigger::spiral(crate::enums::SpiralType::Depression),
timestamp,
"Depression-loneliness spiral indicators detected",
));
}
alerts
}
#[must_use]
pub fn get_effective(&self, path: StatePath) -> f64 {
use crate::enums::{
DispositionPath, HexacoPath, MentalHealthPath, MoodPath, NeedsPath,
PersonCharacteristicsPath, SocialCognitionPath,
};
let state = &self.individual_state;
let value: f32 = match path {
StatePath::Hexaco(p) => match p {
HexacoPath::HonestyHumility => state.hexaco().honesty_humility(),
HexacoPath::Neuroticism => state.hexaco().neuroticism(),
HexacoPath::Extraversion => state.hexaco().extraversion(),
HexacoPath::Agreeableness => state.hexaco().agreeableness(),
HexacoPath::Conscientiousness => state.hexaco().conscientiousness(),
HexacoPath::Openness => state.hexaco().openness(),
},
StatePath::Mood(p) => match p {
MoodPath::Valence => state.mood().valence_effective(),
MoodPath::Arousal => state.mood().arousal_effective(),
MoodPath::Dominance => state.mood().dominance_effective(),
},
StatePath::Needs(p) => match p {
NeedsPath::Stress => state.needs().stress_effective(),
NeedsPath::Fatigue => state.needs().fatigue_effective(),
NeedsPath::Purpose => state.needs().purpose_effective(),
},
StatePath::SocialCognition(p) => match p {
SocialCognitionPath::Loneliness => state.social_cognition().loneliness_effective(),
SocialCognitionPath::PerceivedReciprocalCaring => state
.social_cognition()
.perceived_reciprocal_caring_effective(),
SocialCognitionPath::PerceivedLiability => {
state.social_cognition().perceived_liability_effective()
}
SocialCognitionPath::SelfHate => state.social_cognition().self_hate_effective(),
SocialCognitionPath::PerceivedCompetence => {
state.social_cognition().perceived_competence_effective()
}
},
StatePath::MentalHealth(p) => match p {
MentalHealthPath::Depression => state.mental_health().depression_effective(),
MentalHealthPath::AcquiredCapability => {
state.mental_health().acquired_capability_effective()
}
MentalHealthPath::InterpersonalHopelessness => {
state.mental_health().interpersonal_hopelessness_effective()
}
MentalHealthPath::ThwartedBelongingness => state.compute_thwarted_belongingness(),
MentalHealthPath::PerceivedBurdensomeness => {
state.compute_perceived_burdensomeness()
}
MentalHealthPath::SuicidalDesire => state.compute_suicidal_desire(),
MentalHealthPath::AttemptRisk => state.compute_attempt_risk(),
MentalHealthPath::SelfWorth => state.mental_health().self_worth_effective(),
MentalHealthPath::Hopelessness => state.mental_health().hopelessness_effective(),
},
StatePath::Disposition(p) => match p {
DispositionPath::Empathy => state.disposition().empathy_effective(),
DispositionPath::Aggression => state.disposition().aggression_effective(),
DispositionPath::Grievance => state.disposition().grievance_effective(),
DispositionPath::ImpulseControl => state.disposition().impulse_control_effective(),
DispositionPath::Reactance => state.disposition().reactance_effective(),
DispositionPath::TrustorPropensity => {
state.disposition().trustor_propensity_effective()
}
},
StatePath::PersonCharacteristics(p) => match p {
PersonCharacteristicsPath::SocialCapital => {
state.person_characteristics().social_capital_effective()
}
PersonCharacteristicsPath::CognitiveAbility => {
state.person_characteristics().cognitive_ability_effective()
}
PersonCharacteristicsPath::EmotionalRegulationAssets => state
.person_characteristics()
.emotional_regulation_assets_effective(),
PersonCharacteristicsPath::MaterialSecurity => {
state.person_characteristics().material_security_effective()
}
PersonCharacteristicsPath::ExperienceDiversity => state
.person_characteristics()
.experience_diversity_effective(),
PersonCharacteristicsPath::BaselineMotivation => state
.person_characteristics()
.baseline_motivation_effective(),
PersonCharacteristicsPath::PersistenceTendency => state
.person_characteristics()
.persistence_tendency_effective(),
PersonCharacteristicsPath::CuriosityTendency => state
.person_characteristics()
.curiosity_tendency_effective(),
PersonCharacteristicsPath::Resource => state.person_characteristics().resource(),
PersonCharacteristicsPath::Force => state.person_characteristics().force(),
},
};
f64::from(value)
}
}
impl Clone for ComputedState {
fn clone(&self) -> Self {
ComputedState {
individual_state: self.individual_state.clone(),
age_at_timestamp: self.age_at_timestamp,
life_stage: self.life_stage,
regression_quality: self.regression_quality,
alerts: match self.alerts.get() {
Some(v) => {
let cell = std::cell::OnceCell::new();
let _ = cell.set(v.clone());
cell
}
None => std::cell::OnceCell::new(),
},
interpretations: self.interpretations.clone(),
summary: self.summary.clone(),
delta_summary: self.delta_summary.clone(),
}
}
}
fn collect_base_shift_records(
events: &[&TimestampedEvent],
entity: &Entity,
query_timestamp: Timestamp,
is_forward: bool,
) -> Vec<BaseShiftRecord> {
if !is_forward {
return Vec::new();
}
let reference_timestamp = entity
.birth_date()
.unwrap_or_else(|| Timestamp::from_ymd_hms(1970, 1, 1, 0, 0, 0));
let mut records = Vec::new();
let mut cumulative_positive: HashMap<HexacoPath, f32> = HashMap::new();
let mut cumulative_negative: HashMap<HexacoPath, f32> = HashMap::new();
for te in events {
let event = te.event();
if !event.has_base_shifts() {
continue;
}
if te.timestamp() > query_timestamp {
continue;
}
let age_at_event = if let Some(birth_date) = entity.birth_date() {
if te.timestamp() >= birth_date {
(te.timestamp() - birth_date).as_years() as u16
} else {
0
}
} else {
entity.age().as_years() as u16
};
let event_duration = if te.timestamp() >= reference_timestamp {
te.timestamp() - reference_timestamp
} else {
Duration::zero()
};
for (trait_path, raw_amount) in event.base_shifts() {
let existing = if *raw_amount > 0.0 {
*cumulative_positive.get(trait_path).unwrap_or(&0.0)
} else {
*cumulative_negative.get(trait_path).unwrap_or(&0.0)
};
let modified = apply_formative_modifiers(
*raw_amount,
*trait_path,
age_at_event,
existing,
entity.species(),
);
if modified.abs() < f32::EPSILON {
continue;
}
let record = BaseShiftRecord::new(event_duration, *trait_path, modified);
if modified > 0.0 {
*cumulative_positive.entry(*trait_path).or_insert(0.0) += modified.abs();
} else {
*cumulative_negative.entry(*trait_path).or_insert(0.0) += modified.abs();
}
records.push(record);
}
}
records
}
fn apply_base_shifts_to_state(
mut state: IndividualState,
shift_records: &[BaseShiftRecord],
query_timestamp: Timestamp,
) -> IndividualState {
if shift_records.is_empty() {
return state;
}
let reference = Timestamp::from_ymd_hms(1970, 1, 1, 0, 0, 0);
let query_duration = if query_timestamp >= reference {
query_timestamp - reference
} else {
Duration::zero()
};
for trait_path in HexacoPath::all() {
let trait_records: Vec<_> = shift_records
.iter()
.filter(|r| r.trait_path() == trait_path)
.cloned()
.collect();
if trait_records.is_empty() {
continue;
}
let current_base = match trait_path {
HexacoPath::Openness => state.hexaco().openness(),
HexacoPath::Conscientiousness => state.hexaco().conscientiousness(),
HexacoPath::Extraversion => state.hexaco().extraversion(),
HexacoPath::Agreeableness => state.hexaco().agreeableness(),
HexacoPath::Neuroticism => state.hexaco().neuroticism(),
HexacoPath::HonestyHumility => state.hexaco().honesty_humility(),
};
let effective = effective_base_at(current_base, &trait_records, query_duration);
match trait_path {
HexacoPath::Openness => state.hexaco_mut().set_openness(effective),
HexacoPath::Conscientiousness => state.hexaco_mut().set_conscientiousness(effective),
HexacoPath::Extraversion => state.hexaco_mut().set_extraversion(effective),
HexacoPath::Agreeableness => state.hexaco_mut().set_agreeableness(effective),
HexacoPath::Neuroticism => state.hexaco_mut().set_neuroticism(effective),
HexacoPath::HonestyHumility => state.hexaco_mut().set_honesty_humility(effective),
}
}
state
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::EntityBuilder;
use crate::enums::{EventType, RelationshipSchema, Species};
use crate::event::EventBuilder;
use crate::memory::{MemoryEntry, MemoryLayer, MemoryLayers};
fn create_simulation() -> Simulation {
let reference = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
Simulation::new(reference)
}
fn create_human(id: &str) -> crate::entity::Entity {
EntityBuilder::new()
.id(id)
.species(Species::Human)
.age(crate::types::Duration::years(30))
.build()
.unwrap()
}
#[test]
fn entity_query_handle_entity_id() {
let sim = create_simulation();
let handle = EntityQueryHandle::new(&sim, EntityId::new("test").unwrap());
assert_eq!(handle.entity_id().as_str(), "test");
}
#[test]
fn entity_query_handle_anchor_timestamp() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = Timestamp::from_ymd_hms(2024, 1, 15, 12, 0, 0);
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
assert_eq!(handle.anchor_timestamp(), Some(anchor));
}
#[test]
fn state_at_anchor_returns_original_state() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let _state = handle.state_at(anchor);
}
#[test]
fn state_at_forward_applies_decay() {
let mut sim = create_simulation();
let mut entity = create_human("person_001");
entity
.individual_state_mut()
.mood_mut()
.add_valence_delta(0.5);
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let future = anchor + Duration::weeks(1);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let computed = handle.state_at(future);
let valence = computed.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
assert!(valence < 0.1); }
#[test]
fn trust_decision_helpers_wire_propensity_and_relationship() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let alice = create_human("alice");
let bob = create_human("bob");
sim.add_entity(alice, reference);
sim.add_entity(bob, reference);
let alice_id = EntityId::new("alice").unwrap();
let bob_id = EntityId::new("bob").unwrap();
let formed = reference;
let rel_id = sim.add_relationship(
alice_id.clone(),
bob_id.clone(),
RelationshipSchema::Peer,
formed,
);
let support_time = reference + Duration::days(3);
let support = EventBuilder::new(EventType::ReceiveSupportEmotional)
.source(bob_id.clone())
.target(alice_id.clone())
.severity(0.7)
.build()
.unwrap();
sim.add_event(support, support_time);
let context = TrustContext::new();
let life_domain = LifeDomain::Relationship;
let handle = sim.entity(&alice_id).unwrap();
let decision_from_handle = handle
.trust_decision_for(&bob_id, support_time, &context, life_domain, None)
.unwrap();
let decision_from_sim = sim
.trust_decision_at(&alice_id, &bob_id, support_time, &context, life_domain, None)
.unwrap();
assert_eq!(decision_from_handle, decision_from_sim);
let support = decision_from_handle.support_willingness();
assert!(support.is_finite());
assert!((0.0..=1.0).contains(&support));
let rel = sim.get_relationship(&rel_id).unwrap().relationship();
let direct = rel.compute_trust_decision(
Direction::AToB,
handle
.state_at(support_time)
.individual_state()
.disposition()
.trustor_propensity_effective(),
&context,
life_domain,
None,
);
assert_eq!(decision_from_handle, direct);
}
#[test]
fn trust_decision_for_returns_none_without_relationship() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let alice = create_human("alice");
let bob = create_human("bob");
sim.add_entity(alice, reference);
sim.add_entity(bob, reference);
let alice_id = EntityId::new("alice").unwrap();
let bob_id = EntityId::new("bob").unwrap();
let handle = sim.entity(&alice_id).unwrap();
let decision = handle.trust_decision_for(
&bob_id,
reference,
&TrustContext::new(),
LifeDomain::Relationship,
None,
);
assert!(decision.is_none());
}
#[test]
fn trust_decision_for_uses_b_to_a_direction() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let alice = create_human("alice");
let bob = create_human("bob");
sim.add_entity(alice, reference);
sim.add_entity(bob, reference);
let alice_id = EntityId::new("alice").unwrap();
let bob_id = EntityId::new("bob").unwrap();
sim.add_relationship(
alice_id.clone(),
bob_id.clone(),
RelationshipSchema::Peer,
reference,
);
let handle = sim.entity(&bob_id).unwrap();
let decision = handle.trust_decision_for(
&alice_id,
reference,
&TrustContext::new(),
LifeDomain::Relationship,
None,
);
assert!(decision.is_some());
}
#[test]
fn merge_memory_layers_copies_anchor_and_computed_memories() {
let mut anchor = MemoryLayers::new();
let mut computed = MemoryLayers::new();
anchor.add(
MemoryLayer::Immediate,
MemoryEntry::new(Duration::days(1), "anchor_immediate"),
);
anchor.add(
MemoryLayer::ShortTerm,
MemoryEntry::new(Duration::days(2), "anchor_short_term"),
);
anchor.add(
MemoryLayer::LongTerm,
MemoryEntry::new(Duration::days(3), "anchor_long_term"),
);
anchor.add(
MemoryLayer::Legacy,
MemoryEntry::new(Duration::days(4), "anchor_legacy"),
);
computed.add(
MemoryLayer::Immediate,
MemoryEntry::new(Duration::days(5), "computed_immediate"),
);
computed.add(
MemoryLayer::ShortTerm,
MemoryEntry::new(Duration::days(6), "computed_short_term"),
);
computed.add(
MemoryLayer::LongTerm,
MemoryEntry::new(Duration::days(7), "computed_long_term"),
);
computed.add(
MemoryLayer::Legacy,
MemoryEntry::new(Duration::days(8), "computed_legacy"),
);
let merged = merge_memory_layers(&anchor, &computed);
assert_eq!(merged.immediate_count(), 2);
assert_eq!(merged.short_term_count(), 2);
assert_eq!(merged.long_term_count(), 2);
assert_eq!(merged.legacy_count(), 2);
assert_eq!(merged.total_count(), 8);
assert!(merged
.immediate()
.iter()
.any(|memory| memory.summary() == "anchor_immediate"));
assert!(merged
.immediate()
.iter()
.any(|memory| memory.summary() == "computed_immediate"));
}
#[test]
fn state_at_backward_regresses_state() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
sim.add_entity(entity, anchor);
let past = Timestamp::from_ymd_hms(2024, 5, 1, 0, 0, 0);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let _state = handle.state_at(past);
}
#[test]
fn computed_state_accessors() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let _ = state.individual_state();
let _ = state.age_at_timestamp();
let _ = state.life_stage();
let _ = state.regression_quality();
}
#[test]
fn estimate_relationship_quality_increases_with_attachments() {
use crate::types::RelationshipId;
let mut entity = create_human("person_001");
let relationship_id = RelationshipId::new("rel_001_002").unwrap();
entity.relationship_slots_mut()[0].attach_for_test(relationship_id);
let quality = estimate_relationship_quality(&entity);
assert!(quality > 0.3);
}
#[test]
fn computed_state_alerts_lazy() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let alerts = state.alerts();
assert!(alerts.is_empty());
let alerts2 = state.alerts();
assert!(alerts2.is_empty());
}
fn computed_state_with(individual_state: IndividualState) -> ComputedState {
ComputedState {
individual_state,
age_at_timestamp: Duration::days(1),
life_stage: LifeStage::Adult,
regression_quality: RegressionQuality::Exact,
alerts: std::cell::OnceCell::new(),
interpretations: HashMap::new(),
summary: String::new(),
delta_summary: None,
}
}
#[test]
fn computed_state_alerts_high_risk_paths() {
let social = crate::state::SocialCognition::new()
.with_loneliness_base(1.0)
.with_perceived_reciprocal_caring_base(0.0)
.with_perceived_liability_base(1.0)
.with_self_hate_base(1.0);
let mental = crate::state::MentalHealth::new()
.with_interpersonal_hopelessness_base(1.0)
.with_acquired_capability_base(1.0)
.with_depression_base(0.2);
let individual_state = IndividualState::new()
.with_social_cognition(social)
.with_mental_health(mental);
let computed = computed_state_with(individual_state);
let alerts = computed.alerts();
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire), _)
)));
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk), _)
)));
}
#[test]
fn computed_state_alerts_warning_paths() {
let social = crate::state::SocialCognition::new()
.with_loneliness_base(0.9)
.with_perceived_reciprocal_caring_base(0.1)
.with_perceived_liability_base(0.9)
.with_self_hate_base(0.9);
let mental = crate::state::MentalHealth::new()
.with_interpersonal_hopelessness_base(0.8)
.with_acquired_capability_base(0.8)
.with_depression_base(0.2);
let individual_state = IndividualState::new()
.with_social_cognition(social)
.with_mental_health(mental);
let computed = computed_state_with(individual_state);
let alerts = computed.alerts();
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire), _)
)));
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk), _)
)));
}
#[test]
fn computed_state_alerts_detect_spirals() {
let needs = crate::state::Needs::new()
.with_stress_base(0.9)
.with_fatigue_base(0.8);
let disposition = crate::state::Disposition::new().with_impulse_control_base(0.2);
let social = crate::state::SocialCognition::new().with_loneliness_base(0.8);
let mental = crate::state::MentalHealth::new().with_depression_base(0.8);
let individual_state = IndividualState::new()
.with_needs(needs)
.with_disposition(disposition)
.with_social_cognition(social)
.with_mental_health(mental);
let computed = computed_state_with(individual_state);
let alerts = computed.alerts();
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::SpiralDetected(crate::enums::SpiralType::Stress)
)));
assert!(alerts.iter().any(|a| matches!(
a.trigger(),
AlertTrigger::SpiralDetected(crate::enums::SpiralType::Depression)
)));
}
#[test]
fn build_trust_context_uses_event_microsystem() {
use crate::event::EventBuilder;
use crate::enums::EventType;
let mut sim = create_simulation();
let mut entity = create_human("person_001");
let anchor = sim.reference_date();
let micro_id = crate::types::MicrosystemId::new("work_ctx").unwrap();
let mut work = crate::context::WorkContext::default();
work.warmth = 0.8;
work.hostility = 0.1;
work.interaction_profile.interaction_frequency = 0.9;
work.interaction_profile.interaction_complexity = 0.9;
entity
.context_mut()
.add_microsystem(micro_id.clone(), crate::context::Microsystem::new_work(work));
sim.add_entity(entity.clone(), anchor);
let handle = sim.entity(&entity.id()).unwrap();
let event = EventBuilder::new(EventType::ReceiveSupportEmotional)
.context(micro_id)
.build()
.unwrap();
let context = handle.build_trust_context(&entity, &event);
assert!(context.time_pressure() > 0.5);
assert!(context.social_norms() > 0.5);
}
#[test]
fn build_trust_context_missing_microsystem_keeps_defaults() {
use crate::event::EventBuilder;
use crate::enums::EventType;
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity.clone(), anchor);
let handle = sim.entity(&entity.id()).unwrap();
let micro_id = crate::types::MicrosystemId::new("unknown_ctx").unwrap();
let event = EventBuilder::new(EventType::ReceiveSupportEmotional)
.context(micro_id)
.build()
.unwrap();
let context = handle.build_trust_context(&entity, &event);
assert!((context.time_pressure() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn computed_state_get_effective_mood() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let valence = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
assert!(valence >= -1.0 && valence <= 1.0);
let arousal = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Arousal));
assert!(arousal >= -1.0 && arousal <= 1.0);
let dominance = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Dominance));
assert!(dominance >= -1.0 && dominance <= 1.0);
}
#[test]
fn computed_state_emotion_membership_sums_to_one() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let membership = state.emotion_membership();
let total: f64 = membership.values().copied().sum();
assert!((total - 1.0).abs() < 1e-6);
}
#[test]
fn computed_state_get_effective_needs() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::{NeedsPath, SocialCognitionPath};
let loneliness =
state.get_effective(StatePath::SocialCognition(SocialCognitionPath::Loneliness));
assert!(loneliness >= 0.0 && loneliness <= 1.0);
let prc = state.get_effective(StatePath::SocialCognition(
SocialCognitionPath::PerceivedReciprocalCaring,
));
assert!(prc >= 0.0 && prc <= 1.0);
let liability = state.get_effective(StatePath::SocialCognition(
SocialCognitionPath::PerceivedLiability,
));
assert!(liability >= 0.0 && liability <= 1.0);
let self_hate =
state.get_effective(StatePath::SocialCognition(SocialCognitionPath::SelfHate));
assert!(self_hate >= 0.0 && self_hate <= 1.0);
let perceived_competence = state.get_effective(StatePath::SocialCognition(
SocialCognitionPath::PerceivedCompetence,
));
assert!(perceived_competence >= 0.0 && perceived_competence <= 1.0);
let stress = state.get_effective(StatePath::Needs(NeedsPath::Stress));
assert!(stress >= 0.0 && stress <= 1.0);
let fatigue = state.get_effective(StatePath::Needs(NeedsPath::Fatigue));
assert!(fatigue >= 0.0 && fatigue <= 1.0);
let purpose = state.get_effective(StatePath::Needs(NeedsPath::Purpose));
assert!(purpose >= 0.0 && purpose <= 1.0);
}
#[test]
fn computed_state_get_effective_mental_health() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::MentalHealthPath;
let depression = state.get_effective(StatePath::MentalHealth(MentalHealthPath::Depression));
assert!(depression >= 0.0 && depression <= 1.0);
let ac = state.get_effective(StatePath::MentalHealth(
MentalHealthPath::AcquiredCapability,
));
assert!(ac >= 0.0 && ac <= 1.0);
let ih = state.get_effective(StatePath::MentalHealth(
MentalHealthPath::InterpersonalHopelessness,
));
assert!(ih >= 0.0 && ih <= 1.0);
let tb = state.get_effective(StatePath::MentalHealth(
MentalHealthPath::ThwartedBelongingness,
));
assert!(tb >= 0.0 && tb <= 1.0);
let pb = state.get_effective(StatePath::MentalHealth(
MentalHealthPath::PerceivedBurdensomeness,
));
assert!(pb >= 0.0 && pb <= 1.0);
let desire = state.get_effective(StatePath::MentalHealth(MentalHealthPath::SuicidalDesire));
assert!(desire >= 0.0 && desire <= 1.0);
let risk = state.get_effective(StatePath::MentalHealth(MentalHealthPath::AttemptRisk));
assert!(risk >= 0.0 && risk <= 1.0);
}
#[test]
fn computed_state_get_effective_hexaco() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::HexacoPath;
let hh = state.get_effective(StatePath::Hexaco(HexacoPath::HonestyHumility));
assert!(hh >= 0.0 && hh <= 1.0);
let n = state.get_effective(StatePath::Hexaco(HexacoPath::Neuroticism));
assert!(n >= 0.0 && n <= 1.0);
let e = state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
assert!(e >= 0.0 && e <= 1.0);
let a = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
assert!(a >= 0.0 && a <= 1.0);
let c = state.get_effective(StatePath::Hexaco(HexacoPath::Conscientiousness));
assert!(c >= 0.0 && c <= 1.0);
let o = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
assert!(o >= 0.0 && o <= 1.0);
}
#[test]
fn computed_state_get_effective_disposition() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::DispositionPath;
let empathy = state.get_effective(StatePath::Disposition(DispositionPath::Empathy));
assert!(empathy >= 0.0 && empathy <= 1.0);
let aggression = state.get_effective(StatePath::Disposition(DispositionPath::Aggression));
assert!(aggression >= 0.0 && aggression <= 1.0);
let grievance = state.get_effective(StatePath::Disposition(DispositionPath::Grievance));
assert!(grievance >= 0.0 && grievance <= 1.0);
}
#[test]
fn computed_state_get_effective_person_characteristics() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::PersonCharacteristicsPath;
let sc = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::SocialCapital,
));
assert!(sc >= 0.0 && sc <= 1.0);
let ca = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::CognitiveAbility,
));
assert!(ca >= 0.0 && ca <= 1.0);
let ms = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::MaterialSecurity,
));
assert!(ms >= 0.0 && ms <= 1.0);
}
#[test]
fn state_at_with_event_applies_event() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let entity_id = EntityId::new("person_001").unwrap();
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::EndRelationshipRomantic)
.target(entity_id.clone())
.severity(0.7)
.build()
.unwrap();
let event_time = anchor + Duration::days(1);
sim.add_event(event, event_time);
let query_time = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let _state = handle.state_at(query_time);
}
#[test]
fn age_at_timestamp_forward_with_birth_date() {
let mut sim = create_simulation();
let anchor = sim.reference_date();
let birth_date = anchor - Duration::years(25);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.birth_date(birth_date)
.age(Duration::years(25))
.build()
.unwrap();
sim.add_entity(entity, anchor);
let future = anchor + Duration::years(10);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(future);
assert_eq!(state.age_at_timestamp().as_years(), 35);
}
#[test]
fn age_at_timestamp_forward_without_birth_date_is_constant() {
let mut sim = create_simulation();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(25))
.build()
.unwrap();
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let future = anchor + Duration::years(10);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(future);
assert_eq!(state.age_at_timestamp().as_years(), 25);
}
#[test]
fn age_at_timestamp_backward_with_birth_date() {
let mut sim = create_simulation();
let anchor = sim.reference_date();
let birth_date = anchor - Duration::years(25);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.birth_date(birth_date)
.age(Duration::years(25))
.build()
.unwrap();
sim.add_entity(entity, anchor);
let past = anchor - Duration::years(10);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(past);
assert_eq!(state.age_at_timestamp().as_years(), 15);
}
#[test]
fn age_at_timestamp_backward_without_birth_date_is_constant() {
let mut sim = create_simulation();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(25))
.build()
.unwrap();
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let past = anchor - Duration::years(10);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(past);
assert_eq!(state.age_at_timestamp().as_years(), 25);
}
#[test]
fn life_stage_at_timestamp_with_birth_date() {
let mut sim = create_simulation();
let anchor = sim.reference_date();
let birth_date = anchor - Duration::years(10);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.birth_date(birth_date)
.age(Duration::years(10))
.build()
.unwrap();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
assert_eq!(state.life_stage(), LifeStage::Child);
let future = anchor + Duration::years(10);
let state2 = handle.state_at(future);
assert_eq!(state2.life_stage(), LifeStage::YoungAdult);
}
#[test]
fn life_stage_at_timestamp_without_birth_date_is_constant() {
let mut sim = create_simulation();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(10))
.build()
.unwrap();
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
assert_eq!(state.life_stage(), LifeStage::Child);
let future = anchor + Duration::years(10);
let state2 = handle.state_at(future);
assert_eq!(state2.life_stage(), LifeStage::Child);
}
#[test]
fn regression_quality_forward_is_exact() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let future = anchor + Duration::days(30);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(future);
assert!(state.regression_quality().is_exact());
}
#[test]
fn computed_state_debug() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let debug = format!("{:?}", state);
assert!(debug.contains("ComputedState"));
}
#[test]
fn computed_state_clone() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let cloned = state.clone();
assert_eq!(state.age_at_timestamp(), cloned.age_at_timestamp());
}
#[test]
#[should_panic(expected = "EntityQueryHandle created for non-existent entity")]
fn state_at_nonexistent_entity_panics() {
let sim = create_simulation();
let unknown = EntityId::new("unknown").unwrap();
let handle = EntityQueryHandle::new(&sim, unknown);
let _state = handle.state_at(sim.reference_date());
}
#[test]
fn anchor_timestamp_nonexistent_entity() {
let sim = create_simulation();
let unknown = EntityId::new("unknown").unwrap();
let handle = EntityQueryHandle::new(&sim, unknown);
assert!(handle.anchor_timestamp().is_none());
}
#[test]
fn state_at_backward_with_events() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let entity_id = EntityId::new("person_001").unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::EndRelationshipRomantic)
.target(entity_id.clone())
.severity(0.7)
.build()
.unwrap();
let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
sim.add_event(event, event_time);
let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let handle = sim.entity(&entity_id).unwrap();
let computed = handle.state_at(past);
assert!(computed.regression_quality().is_exact());
}
#[test]
fn computed_state_get_effective_mental_health_all_paths() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::MentalHealthPath;
let self_worth = state.get_effective(StatePath::MentalHealth(MentalHealthPath::SelfWorth));
assert!(self_worth >= 0.0 && self_worth <= 1.0);
let hopelessness =
state.get_effective(StatePath::MentalHealth(MentalHealthPath::Hopelessness));
assert!(hopelessness >= 0.0 && hopelessness <= 1.0);
}
#[test]
fn computed_state_get_effective_disposition_all_paths() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::DispositionPath;
let impulse = state.get_effective(StatePath::Disposition(DispositionPath::ImpulseControl));
assert!(impulse >= 0.0 && impulse <= 1.0);
let reactance = state.get_effective(StatePath::Disposition(DispositionPath::Reactance));
assert!(reactance >= 0.0 && reactance <= 1.0);
let trust = state.get_effective(StatePath::Disposition(DispositionPath::TrustorPropensity));
assert!(trust >= 0.0 && trust <= 1.0);
}
#[test]
fn computed_state_get_effective_person_characteristics_all_paths() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
use crate::enums::PersonCharacteristicsPath;
let era = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::EmotionalRegulationAssets,
));
assert!(era >= 0.0 && era <= 1.0);
let ed = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::ExperienceDiversity,
));
assert!(ed >= 0.0 && ed <= 1.0);
let bm = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::BaselineMotivation,
));
assert!(bm >= 0.0 && bm <= 1.0);
let pt = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::PersistenceTendency,
));
assert!(pt >= 0.0 && pt <= 1.0);
let ct = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::CuriosityTendency,
));
assert!(ct >= 0.0 && ct <= 1.0);
let _resource = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::Resource,
));
let _force = state.get_effective(StatePath::PersonCharacteristics(
PersonCharacteristicsPath::Force,
));
}
#[test]
fn memories_at_returns_empty_for_new_entity() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let memories = handle.memories_at(anchor);
assert!(memories.is_empty());
}
#[test]
fn memories_at_returns_empty_for_nonexistent_entity() {
let sim = create_simulation();
let unknown = EntityId::new("unknown").unwrap();
let handle = EntityQueryHandle::new(&sim, unknown);
let memories = handle.memories_at(sim.reference_date());
assert!(memories.is_empty());
}
#[test]
fn memories_at_filters_by_timestamp() {
use crate::memory::MemoryTag;
let mut sim = create_simulation();
let mut entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(25))
.build()
.unwrap();
entity.create_memory(
"First memory at age 25",
vec![],
vec![MemoryTag::Personal],
0.5,
None,
);
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let memories_at_anchor = handle.memories_at(anchor);
assert_eq!(memories_at_anchor.len(), 1);
let past = anchor - Duration::years(10);
let memories_past = handle.memories_at(past);
assert!(memories_past.is_empty());
}
#[test]
fn memories_at_forward_in_time() {
use crate::memory::MemoryTag;
let mut sim = create_simulation();
let mut entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(25))
.build()
.unwrap();
entity.create_memory(
"Memory at age 25",
vec![],
vec![MemoryTag::Personal],
0.5,
None,
);
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let future = anchor + Duration::years(10);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let memories_future = handle.memories_at(future);
assert_eq!(memories_future.len(), 1);
}
#[test]
fn age_computed_from_birth_date() {
let mut sim = create_simulation();
let birth = Timestamp::from_ymd_hms(1990, 6, 15, 0, 0, 0);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(birth)
.build()
.unwrap();
let anchor = sim.reference_date(); sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let age_at_anchor = state.age_at_timestamp();
assert!(age_at_anchor.as_years() >= 33);
assert!(age_at_anchor.as_years() <= 34);
let future = anchor + Duration::years(10);
let future_state = handle.state_at(future);
let age_at_future = future_state.age_at_timestamp();
assert!(age_at_future.as_years() >= 43);
assert!(age_at_future.as_years() <= 44);
}
#[test]
fn age_before_birth_returns_zero() {
let birth = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(birth)
.build()
.unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let mut sim = Simulation::new(Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0));
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let before_birth = Timestamp::from_ymd_hms(1990, 1, 1, 0, 0, 0);
let state = handle.state_at(before_birth);
assert!(state.age_at_timestamp().is_zero());
}
#[test]
fn regression_through_trauma_is_approximate() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let entity_id = EntityId::new("person_001").unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
sim.add_entity(entity, anchor);
let trauma_event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.8)
.build()
.unwrap();
let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
sim.add_event(trauma_event, event_time);
let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(past);
assert!(state.regression_quality().is_approximate());
}
#[test]
fn regression_without_trauma_is_exact() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let entity_id = EntityId::new("person_001").unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
sim.add_entity(entity, anchor);
let social_event = EventBuilder::new(EventType::EndRelationshipRomantic)
.target(entity_id.clone())
.severity(0.5)
.build()
.unwrap();
let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
sim.add_event(social_event, event_time);
let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(past);
assert!(state.regression_quality().is_exact());
}
#[test]
fn estimate_relationship_quality_baseline() {
let entity = create_human("person_001");
let baseline = estimate_relationship_quality(&entity);
assert!((baseline - 0.3).abs() < f64::EPSILON);
}
#[test]
fn computed_state_clone_with_cached_alerts() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let alerts1 = state.alerts();
assert!(alerts1.is_empty());
let cloned = state.clone();
let alerts2 = cloned.alerts();
assert!(alerts2.is_empty());
}
#[test]
fn computed_state_clone_without_cached_alerts() {
let mut sim = create_simulation();
let entity = create_human("person_001");
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
let state = handle.state_at(anchor);
let cloned = state.clone();
let alerts1 = state.alerts();
let alerts2 = cloned.alerts();
assert!(alerts1.is_empty());
assert!(alerts2.is_empty());
}
#[test]
fn memories_at_before_birth_returns_empty() {
use crate::memory::MemoryTag;
let mut sim = create_simulation();
let mut entity = EntityBuilder::new()
.id("child_001")
.species(Species::Human)
.age(Duration::years(5))
.build()
.unwrap();
entity.create_memory(
"Memory at age 5",
vec![],
vec![MemoryTag::Personal],
0.5,
None,
);
let anchor = sim.reference_date(); sim.add_entity(entity, anchor);
let past = anchor - Duration::years(10);
let handle = sim.entity(&EntityId::new("child_001").unwrap()).unwrap();
let memories = handle.memories_at(past);
assert!(memories.is_empty());
}
#[test]
fn developmental_effects_entity_without_birth_date_uses_anchor_age() {
let mut sim = create_simulation();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(30))
.build()
.unwrap();
let entity_id = EntityId::new("person_001").unwrap();
let anchor = sim.reference_date();
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.5)
.build()
.unwrap();
let event_time = anchor + Duration::days(10);
sim.add_event(event, event_time);
let query_time = anchor + Duration::days(20);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(query_time);
let valence = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
assert!(valence >= -1.0 && valence <= 1.0);
}
#[test]
fn developmental_effects_event_after_birth_date_uses_birth_age() {
let birth_date = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(birth_date)
.build()
.unwrap();
let entity_id = EntityId::new("person_001").unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let mut sim = Simulation::new(anchor);
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::AchieveGoalMajor)
.target(entity_id.clone())
.severity(0.5)
.build()
.unwrap();
let event_time = anchor + Duration::days(10);
sim.add_event(event, event_time);
let query_time = anchor + Duration::days(20);
let handle = sim.entity(&entity_id).unwrap();
assert_eq!(
handle
.get_sorted_events_for_range(anchor, query_time, true)
.len(),
1
);
let state = handle.state_at(query_time);
assert!(!state.age_at_timestamp().is_zero());
}
#[test]
fn developmental_effects_event_before_birth_date() {
let birth_date = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(birth_date)
.build()
.unwrap();
let entity_id = EntityId::new("person_001").unwrap();
let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let mut sim = Simulation::new(anchor);
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::AchieveGoalMajor)
.target(entity_id.clone())
.severity(0.5)
.build()
.unwrap();
let event_time = Timestamp::from_ymd_hms(1990, 1, 1, 0, 0, 0); sim.add_event(event, event_time);
let query_time = Timestamp::from_ymd_hms(1980, 1, 1, 0, 0, 0);
let handle = sim.entity(&entity_id).unwrap();
assert_eq!(
handle
.get_sorted_events_for_range(anchor, query_time, false)
.len(),
1
);
let state = handle.state_at(query_time);
assert!(state.age_at_timestamp().is_zero());
}
#[test]
fn formative_event_shifts_personality() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let handle = sim.entity(&entity_id).unwrap();
let baseline_state = handle.state_at(anchor);
let baseline_agreeableness =
baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.9)
.with_base_shift(HexacoPath::Agreeableness, -0.15)
.build()
.unwrap();
let event_time = anchor + Duration::days(1);
sim.add_event(event, event_time);
let later = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let later_state = handle.state_at(later);
let later_agreeableness =
later_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
assert!(later_agreeableness < baseline_agreeableness);
}
#[test]
fn formative_event_backward_query_no_shift() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference + Duration::days(10);
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.9)
.with_base_shift(HexacoPath::Agreeableness, -0.15)
.build()
.unwrap();
let event_time = reference + Duration::days(5);
sim.add_event(event, event_time);
let earlier = reference + Duration::days(1);
let handle = sim.entity(&entity_id).unwrap();
let earlier_state = handle.state_at(earlier);
let earlier_agreeableness =
earlier_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
let anchor_state = handle.state_at(anchor);
let anchor_agreeableness =
anchor_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
assert!((earlier_agreeableness - anchor_agreeableness).abs() < 0.1);
}
#[test]
fn formative_event_multiple_shifts_same_trait() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let handle = sim.entity(&entity_id).unwrap();
let baseline_state = handle.state_at(anchor);
let baseline_agreeableness =
baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
let event1 = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.8)
.with_base_shift(HexacoPath::Agreeableness, -0.10)
.build()
.unwrap();
sim.add_event(event1, anchor + Duration::days(1));
let event2 = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.7)
.with_base_shift(HexacoPath::Agreeableness, -0.08)
.build()
.unwrap();
sim.add_event(event2, anchor + Duration::days(2));
let later = anchor + Duration::days(3);
let handle = sim.entity(&entity_id).unwrap();
let later_state = handle.state_at(later);
let later_agreeableness =
later_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
assert!(later_agreeableness < baseline_agreeableness);
}
#[test]
fn formative_event_no_shift_leaves_trait_unchanged() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let handle = sim.entity(&entity_id).unwrap();
let baseline_state = handle.state_at(anchor);
let baseline_openness =
baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.8)
.build()
.unwrap();
sim.add_event(event, anchor + Duration::days(1));
let later = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let later_state = handle.state_at(later);
let later_openness = later_state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
assert!((later_openness - baseline_openness).abs() < 0.01);
}
#[test]
fn collect_base_shifts_empty_for_backward_query() {
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.build()
.unwrap();
let records = collect_base_shift_records(
&[],
&entity,
Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
false, );
assert!(records.is_empty());
}
#[test]
fn apply_base_shifts_empty_records_returns_unchanged() {
let state = IndividualState::new();
let original_openness = state.hexaco().openness();
let result =
apply_base_shifts_to_state(state, &[], Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0));
assert!((result.hexaco().openness() - original_openness).abs() < f32::EPSILON);
}
#[test]
fn formative_event_all_hexaco_traits() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.9)
.with_base_shift(HexacoPath::Openness, 0.10)
.with_base_shift(HexacoPath::Conscientiousness, -0.08)
.with_base_shift(HexacoPath::Extraversion, 0.12)
.with_base_shift(HexacoPath::Agreeableness, -0.15)
.with_base_shift(HexacoPath::Neuroticism, 0.20)
.with_base_shift(HexacoPath::HonestyHumility, -0.05)
.build()
.unwrap();
sim.add_event(event, anchor + Duration::days(1));
let later = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(later);
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Conscientiousness));
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Neuroticism));
let _ = state.get_effective(StatePath::Hexaco(HexacoPath::HonestyHumility));
}
#[test]
fn formative_event_positive_and_negative_shifts() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let handle = sim.entity(&entity_id).unwrap();
let baseline_state = handle.state_at(anchor);
let baseline_extraversion =
baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
let event = EventBuilder::new(EventType::AchieveGoalMajor)
.target(entity_id.clone())
.severity(0.7)
.with_base_shift(HexacoPath::Extraversion, 0.15)
.build()
.unwrap();
sim.add_event(event, anchor + Duration::days(1));
let later = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let later_state = handle.state_at(later);
let later_extraversion =
later_state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
assert!(later_extraversion > baseline_extraversion);
}
#[test]
fn formative_event_entity_without_birth_date() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(Duration::years(30))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.8)
.with_base_shift(HexacoPath::Openness, -0.10)
.build()
.unwrap();
sim.add_event(event, anchor + Duration::days(1));
let later = anchor + Duration::days(2);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(later);
let openness = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
assert!(openness >= -1.0 && openness <= 1.0);
}
#[test]
fn formative_event_after_query_timestamp_ignored() {
let mut sim = create_simulation();
let reference = sim.reference_date();
let entity = EntityBuilder::new()
.id("person_001")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(reference - Duration::years(25))
.build()
.unwrap();
let entity_id = entity.id().clone();
let anchor = reference;
sim.add_entity(entity, anchor);
let handle = sim.entity(&entity_id).unwrap();
let baseline_state = handle.state_at(anchor);
let baseline_agreeableness =
baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.target(entity_id.clone())
.severity(0.9)
.with_base_shift(HexacoPath::Agreeableness, -0.30)
.build()
.unwrap();
sim.add_event(event, anchor + Duration::days(10));
let before_event = anchor + Duration::days(5);
let handle = sim.entity(&entity_id).unwrap();
let state = handle.state_at(before_event);
let agreeableness = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
assert!((agreeableness - baseline_agreeableness).abs() < 0.01);
}
#[test]
fn collect_base_shifts_forward_query_with_events() {
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(1999, 1, 1, 0, 0, 0))
.build()
.unwrap();
let records = collect_base_shift_records(
&[],
&entity,
Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
true, );
assert!(records.is_empty());
}
#[test]
fn apply_base_shifts_updates_all_traits() {
use crate::state::BaseShiftRecord;
let state = IndividualState::new();
let original_openness = state.hexaco().openness();
let records = vec![BaseShiftRecord::new(
Duration::days(1),
HexacoPath::Openness,
0.15,
)];
let result = apply_base_shifts_to_state(
state.clone(),
&records,
Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
);
assert!((result.hexaco().openness() - original_openness).abs() > 0.01);
}
#[test]
fn apply_base_shifts_each_hexaco_trait() {
use crate::state::BaseShiftRecord;
for trait_path in HexacoPath::all() {
let state = IndividualState::new();
let records = vec![BaseShiftRecord::new(Duration::days(1), trait_path, 0.10)];
let result = apply_base_shifts_to_state(
state,
&records,
Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
);
let value = match trait_path {
HexacoPath::Openness => result.hexaco().openness(),
HexacoPath::Conscientiousness => result.hexaco().conscientiousness(),
HexacoPath::Extraversion => result.hexaco().extraversion(),
HexacoPath::Agreeableness => result.hexaco().agreeableness(),
HexacoPath::Neuroticism => result.hexaco().neuroticism(),
HexacoPath::HonestyHumility => result.hexaco().honesty_humility(),
};
assert!(value >= -1.0 && value <= 1.0);
}
}
#[test]
fn collect_base_shifts_event_after_query_timestamp_skipped() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(1999, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Agreeableness, -0.20)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 4, 10, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert!(records.is_empty());
}
#[test]
fn collect_base_shifts_event_before_birth_date_age_zero() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(2010, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Neuroticism, 0.25)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2005, 6, 1, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2020, 1, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert_eq!(records.len(), 1);
}
#[test]
fn collect_base_shifts_entity_without_birth_date_uses_anchor_age() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(Duration::years(35))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Conscientiousness, -0.15)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert_eq!(records.len(), 1);
}
#[test]
fn collect_base_shifts_event_before_1970_reference() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(1940, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Extraversion, -0.20)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(1960, 6, 1, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert_eq!(records.len(), 1);
}
#[test]
fn collect_base_shifts_tiny_shift_rounds_to_zero() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(1940, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Extraversion, 1e-8)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2020, 6, 1, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert!(records.is_empty());
}
#[test]
fn collect_base_shifts_positive_cumulative_tracking() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event = EventBuilder::new(EventType::AchieveGoalMajor)
.with_base_shift(HexacoPath::Agreeableness, 0.25)
.build()
.unwrap();
let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
assert_eq!(records.len(), 1);
assert!(records[0].immediate() > 0.0);
}
#[test]
fn collect_base_shifts_multiple_positive_shifts_diminishing() {
use crate::simulation::TimestampedEvent;
let entity = EntityBuilder::new()
.id("test")
.species(Species::Human)
.age(crate::types::Duration::years(30))
.birth_date(Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0))
.build()
.unwrap();
let event1 = EventBuilder::new(EventType::AchieveGoalMajor)
.with_base_shift(HexacoPath::Agreeableness, 0.30)
.build()
.unwrap();
let te1 = TimestampedEvent::new(event1, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
let event2 = EventBuilder::new(EventType::AchieveGoalMajor)
.with_base_shift(HexacoPath::Agreeableness, 0.30)
.build()
.unwrap();
let te2 = TimestampedEvent::new(event2, Timestamp::from_ymd_hms(2024, 2, 15, 0, 0, 0));
let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
let records = collect_base_shift_records(&[&te1, &te2], &entity, query_ts, true);
assert_eq!(records.len(), 2);
assert!(records[1].immediate() < records[0].immediate());
}
#[test]
fn apply_base_shifts_empty_returns_unchanged() {
let state = IndividualState::new();
let original_openness = state.hexaco().openness();
let result = apply_base_shifts_to_state(
state,
&[], Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
);
assert!((result.hexaco().openness() - original_openness).abs() < f32::EPSILON);
}
#[test]
fn apply_base_shifts_query_before_1970() {
use crate::state::BaseShiftRecord;
let state = IndividualState::new();
let records = vec![BaseShiftRecord::new(
Duration::days(1),
HexacoPath::Openness,
0.15,
)];
let result = apply_base_shifts_to_state(
state,
&records,
Timestamp::from_ymd_hms(1950, 1, 1, 0, 0, 0),
);
assert!(result.hexaco().openness() >= -1.0 && result.hexaco().openness() <= 1.0);
}
}