use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MemoryType {
FoodSource,
WaterSource,
Shelter,
Threat,
Individual,
Territory,
Waypoint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryTrace {
pub memory_type: MemoryType,
pub strength: f32,
pub reinforcement_count: u32,
pub valence: f32,
}
impl MemoryTrace {
#[must_use]
pub fn new(memory_type: MemoryType, initial_strength: f32, valence: f32) -> Self {
Self {
memory_type,
strength: initial_strength.clamp(0.0, 1.0),
reinforcement_count: 1,
valence: valence.clamp(-1.0, 1.0),
}
}
pub fn reinforce(&mut self, amount: f32) {
let amount = amount.clamp(0.0, 1.0);
self.reinforcement_count = self.reinforcement_count.saturating_add(1);
let room = 1.0 - self.strength;
self.strength += room * amount * 0.5;
self.strength = self.strength.clamp(0.0, 1.0);
}
pub fn forget(&mut self, dt: f32) {
let reinforcement_protection = 1.0 / (1.0 + self.reinforcement_count as f32 * 0.2);
let decay_rate = 0.01 * reinforcement_protection;
self.strength *= (-decay_rate * dt).exp();
if self.strength < 1e-6 {
self.strength = 0.0;
}
}
#[must_use]
#[inline]
pub fn is_accessible(&self) -> bool {
self.strength > 0.1
}
#[must_use]
#[inline]
pub fn is_aversive(&self) -> bool {
self.valence < -0.3
}
#[must_use]
#[inline]
pub fn is_appetitive(&self) -> bool {
self.valence > 0.3
}
}
#[must_use]
pub fn neophobia_modifier(familiarity: f32) -> f32 {
let familiarity = familiarity.clamp(0.0, 1.0);
1.0 - familiarity * 0.7
}
#[must_use]
pub fn spatial_reliability(memory_strength: f32, environment_stability: f32) -> f32 {
let memory_strength = memory_strength.clamp(0.0, 1.0);
let environment_stability = environment_stability.clamp(0.0, 1.0);
memory_strength * (0.3 + environment_stability * 0.7)
}
#[must_use]
pub fn social_recognition(encounters: u32, time_since_last: f32) -> f32 {
if encounters == 0 {
return 0.0;
}
let buildup = 1.0 - (-0.3 * encounters as f32).exp();
let decay = (-0.01 * time_since_last).exp();
buildup * decay
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_memory_has_strength() {
let m = MemoryTrace::new(MemoryType::FoodSource, 0.8, 0.5);
assert!((m.strength - 0.8).abs() < f32::EPSILON);
assert_eq!(m.reinforcement_count, 1);
}
#[test]
fn reinforcement_increases_strength() {
let mut m = MemoryTrace::new(MemoryType::FoodSource, 0.5, 0.3);
let before = m.strength;
m.reinforce(0.6);
assert!(m.strength > before);
assert_eq!(m.reinforcement_count, 2);
}
#[test]
fn reinforcement_diminishing_returns() {
let mut m = MemoryTrace::new(MemoryType::Shelter, 0.9, 0.5);
let before = m.strength;
m.reinforce(0.5);
let gain = m.strength - before;
assert!(gain < 0.1, "near-max memory should gain little: {gain}");
}
#[test]
fn forgetting_reduces_strength() {
let mut m = MemoryTrace::new(MemoryType::Threat, 0.8, -0.9);
m.forget(50.0);
assert!(m.strength < 0.8);
}
#[test]
fn reinforced_memories_decay_slower() {
let mut fresh = MemoryTrace::new(MemoryType::FoodSource, 0.8, 0.5);
let mut practiced = MemoryTrace::new(MemoryType::FoodSource, 0.8, 0.5);
for _ in 0..10 {
practiced.reinforce(0.3);
}
practiced.strength = 0.8;
fresh.forget(20.0);
practiced.forget(20.0);
assert!(
practiced.strength > fresh.strength,
"practiced should decay slower: practiced={}, fresh={}",
practiced.strength,
fresh.strength
);
}
#[test]
fn inaccessible_when_forgotten() {
let mut m = MemoryTrace::new(MemoryType::WaterSource, 0.5, 0.3);
m.forget(200.0);
assert!(!m.is_accessible());
}
#[test]
fn valence_classification() {
let threat = MemoryTrace::new(MemoryType::Threat, 0.7, -0.8);
assert!(threat.is_aversive());
assert!(!threat.is_appetitive());
let food = MemoryTrace::new(MemoryType::FoodSource, 0.7, 0.8);
assert!(food.is_appetitive());
assert!(!food.is_aversive());
}
#[test]
fn neophobia_decreases_with_familiarity() {
let novel = neophobia_modifier(0.0);
let familiar = neophobia_modifier(0.9);
assert!(novel > familiar, "novel should be scarier");
assert!((novel - 1.0).abs() < f32::EPSILON);
}
#[test]
fn spatial_reliability_depends_on_stability() {
let stable = spatial_reliability(0.8, 0.9);
let unstable = spatial_reliability(0.8, 0.1);
assert!(stable > unstable);
}
#[test]
fn social_recognition_builds_with_encounters() {
let few = social_recognition(1, 0.0);
let many = social_recognition(10, 0.0);
assert!(many > few);
}
#[test]
fn social_recognition_decays_with_time() {
let recent = social_recognition(5, 0.0);
let old = social_recognition(5, 100.0);
assert!(recent > old);
}
#[test]
fn social_recognition_zero_encounters() {
assert_eq!(social_recognition(0, 10.0), 0.0);
}
#[test]
fn serde_roundtrip_memory_type() {
for t in [
MemoryType::FoodSource,
MemoryType::WaterSource,
MemoryType::Shelter,
MemoryType::Threat,
MemoryType::Individual,
MemoryType::Territory,
MemoryType::Waypoint,
] {
let json = serde_json::to_string(&t).unwrap();
let t2: MemoryType = serde_json::from_str(&json).unwrap();
assert_eq!(t, t2);
}
}
#[test]
fn serde_roundtrip_memory_trace() {
let m = MemoryTrace::new(MemoryType::Threat, 0.7, -0.5);
let json = serde_json::to_string(&m).unwrap();
let m2: MemoryTrace = serde_json::from_str(&json).unwrap();
assert!((m.strength - m2.strength).abs() < f32::EPSILON);
assert!((m.valence - m2.valence).abs() < f32::EPSILON);
assert_eq!(m.memory_type, m2.memory_type);
}
}