use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HabitatType {
DenseForest,
Woodland,
OpenGrassland,
Waterhole,
RockyRefuge,
}
#[must_use]
pub fn perceived_risk(predator_density: f32, cover: f32, distance_to_refuge: f32) -> f32 {
let predator_density = predator_density.max(0.0);
let cover = cover.clamp(0.0, 1.0);
let distance_to_refuge = distance_to_refuge.max(0.0);
let exposure = 1.0 - cover * 0.8;
let refuge_penalty = (1.0 + distance_to_refuge).ln() * 0.3;
let base_risk = predator_density * exposure + refuge_penalty;
let sigmoid = base_risk / (1.0 + base_risk);
sigmoid.clamp(0.0, 1.0)
}
#[must_use]
pub fn fear_foraging_efficiency(risk: f32, hunger: f32) -> f32 {
let risk = risk.clamp(0.0, 1.0);
let hunger = hunger.clamp(0.0, 1.0);
let fear_suppression = 1.0 - risk * 0.8;
let hunger_override = 1.0 + hunger * risk * 0.4;
(fear_suppression * hunger_override).clamp(0.0, 1.0)
}
#[must_use]
pub fn group_dilution(group_size: u32, predator_activity: f32) -> f32 {
let group_size = group_size.max(1);
let predator_activity = predator_activity.clamp(0.0, 1.0);
predator_activity / (group_size as f32).sqrt()
}
#[must_use]
pub fn net_habitat_value(resource_quality: f32, risk: f32) -> f32 {
let resource_quality = resource_quality.clamp(0.0, 1.0);
let risk = risk.clamp(0.0, 1.0);
resource_quality - risk * 0.8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposed_riskier_than_hidden() {
let exposed = perceived_risk(0.5, 0.1, 50.0);
let hidden = perceived_risk(0.5, 0.9, 5.0);
assert!(exposed > hidden);
}
#[test]
fn no_predators_low_risk() {
let risk = perceived_risk(0.0, 0.5, 10.0);
assert!(risk < 0.5);
}
#[test]
fn fear_reduces_foraging() {
let safe = fear_foraging_efficiency(0.1, 0.5);
let scary = fear_foraging_efficiency(0.9, 0.5);
assert!(safe > scary);
}
#[test]
fn hunger_overrides_fear() {
let sated = fear_foraging_efficiency(0.8, 0.2);
let starving = fear_foraging_efficiency(0.8, 1.0);
assert!(starving > sated);
}
#[test]
fn group_dilution_effect() {
let solo = group_dilution(1, 0.5);
let herd = group_dilution(50, 0.5);
assert!(solo > herd);
}
#[test]
fn group_dilution_no_predators() {
assert_eq!(group_dilution(10, 0.0), 0.0);
}
#[test]
fn net_value_safe_positive() {
assert!(net_habitat_value(0.5, 0.1) > 0.0);
}
#[test]
fn net_value_dangerous_low() {
let value = net_habitat_value(0.3, 0.9);
assert!(value < 0.0);
}
#[test]
fn serde_roundtrip_habitat_type() {
for h in [
HabitatType::DenseForest,
HabitatType::Woodland,
HabitatType::OpenGrassland,
HabitatType::Waterhole,
HabitatType::RockyRefuge,
] {
let json = serde_json::to_string(&h).unwrap();
let h2: HabitatType = serde_json::from_str(&json).unwrap();
assert_eq!(h, h2);
}
}
}