use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct PreyItem {
pub energy: f32,
pub handling_time: f32,
}
impl PreyItem {
#[must_use]
#[inline]
pub fn profitability(&self) -> f32 {
if self.handling_time <= 0.0 {
return 0.0;
}
self.energy / self.handling_time
}
}
#[must_use]
pub fn should_pursue(item: &PreyItem, current_intake_rate: f32) -> bool {
item.profitability() > current_intake_rate
}
#[must_use]
pub fn should_leave_patch(current_patch_rate: f32, average_habitat_rate: f32) -> bool {
current_patch_rate <= average_habitat_rate
}
#[must_use]
pub fn giving_up_density(predation_risk: f32, metabolic_cost: f32, patch_quality: f32) -> f32 {
let predation_risk = predation_risk.clamp(0.0, 1.0);
let metabolic_cost = metabolic_cost.clamp(0.0, 1.0);
let cost = predation_risk + metabolic_cost;
patch_quality * cost / (1.0 + cost)
}
#[must_use]
pub fn vigilance_fraction(predation_risk: f32, hunger: f32, group_size: u32) -> f32 {
let predation_risk = predation_risk.clamp(0.0, 1.0);
let hunger = hunger.clamp(0.0, 1.0);
let group_size = group_size.max(1);
let base = predation_risk * 0.8;
let hunger_suppression = 1.0 - hunger * 0.6;
let group_dilution = 1.0 / (group_size as f32).sqrt();
(base * hunger_suppression * group_dilution).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profitability_zero_handling() {
let item = PreyItem {
energy: 5.0,
handling_time: 0.0,
};
assert_eq!(item.profitability(), 0.0);
}
#[test]
fn optimal_diet_includes_profitable() {
let rich = PreyItem {
energy: 10.0,
handling_time: 1.0,
};
assert!(should_pursue(&rich, 5.0));
}
#[test]
fn optimal_diet_rejects_poor() {
let poor = PreyItem {
energy: 1.0,
handling_time: 5.0,
};
assert!(!should_pursue(&poor, 5.0));
}
#[test]
fn leave_depleted_patch() {
assert!(should_leave_patch(0.5, 1.0));
}
#[test]
fn stay_in_rich_patch() {
assert!(!should_leave_patch(2.0, 1.0));
}
#[test]
fn higher_risk_higher_gud() {
let safe = giving_up_density(0.1, 0.2, 10.0);
let risky = giving_up_density(0.8, 0.2, 10.0);
assert!(risky > safe);
}
#[test]
fn zero_risk_zero_cost_zero_gud() {
let gud = giving_up_density(0.0, 0.0, 10.0);
assert_eq!(gud, 0.0);
}
#[test]
fn group_reduces_vigilance() {
let solo = vigilance_fraction(0.8, 0.3, 1);
let group = vigilance_fraction(0.8, 0.3, 10);
assert!(solo > group);
}
#[test]
fn hunger_reduces_vigilance() {
let sated = vigilance_fraction(0.8, 0.1, 1);
let hungry = vigilance_fraction(0.8, 0.9, 1);
assert!(sated > hungry);
}
#[test]
fn serde_roundtrip_prey_item() {
let item = PreyItem {
energy: 5.0,
handling_time: 2.0,
};
let json = serde_json::to_string(&item).unwrap();
let item2: PreyItem = serde_json::from_str(&json).unwrap();
assert!((item.energy - item2.energy).abs() < f32::EPSILON);
assert!((item.handling_time - item2.handling_time).abs() < f32::EPSILON);
}
}