use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EcologicalRole {
ApexPredator,
Mesopredator,
Prey,
Parasite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ArmsRaceTrait {
Speed,
Detection,
Toxicity,
Armor,
Endurance,
GroupDefense,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraitMatchup {
pub predator_trait: f32,
pub prey_trait: f32,
pub trait_type: ArmsRaceTrait,
}
impl TraitMatchup {
#[must_use]
pub fn new(trait_type: ArmsRaceTrait, predator_trait: f32, prey_trait: f32) -> Self {
Self {
predator_trait: predator_trait.clamp(0.0, 1.0),
prey_trait: prey_trait.clamp(0.0, 1.0),
trait_type,
}
}
#[must_use]
#[inline]
pub fn predator_advantage(&self) -> f32 {
(self.predator_trait - self.prey_trait).max(0.0)
}
#[must_use]
#[inline]
pub fn prey_advantage(&self) -> f32 {
(self.prey_trait - self.predator_trait).max(0.0)
}
}
#[must_use]
pub fn trait_pressure(own_trait: f32, opponent_trait: f32, selection_intensity: f32) -> f32 {
let own_trait = own_trait.clamp(0.0, 1.0);
let opponent_trait = opponent_trait.clamp(0.0, 1.0);
let selection_intensity = selection_intensity.clamp(0.0, 1.0);
let deficit = (opponent_trait - own_trait).max(0.0);
let raw_pressure = deficit * selection_intensity;
raw_pressure * (1.0 - own_trait * 0.5)
}
#[must_use]
pub fn red_queen_balance(predator_rate: f32, prey_rate: f32) -> f32 {
predator_rate - prey_rate
}
#[must_use]
pub fn encounter_rate(predator_density: f32, prey_density: f32, search_efficiency: f32) -> f32 {
if predator_density <= 0.0 || prey_density <= 0.0 {
return 0.0;
}
let search_efficiency = search_efficiency.clamp(0.0, 1.0);
predator_density * prey_density * search_efficiency
}
#[must_use]
pub fn functional_response_type2(prey_density: f32, attack_rate: f32, handling_time: f32) -> f32 {
if prey_density <= 0.0 || attack_rate <= 0.0 {
return 0.0;
}
(attack_rate * prey_density) / (1.0 + attack_rate * handling_time * prey_density)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn predator_advantage_when_faster() {
let m = TraitMatchup::new(ArmsRaceTrait::Speed, 0.8, 0.5);
assert!((m.predator_advantage() - 0.3).abs() < f32::EPSILON);
assert_eq!(m.prey_advantage(), 0.0);
}
#[test]
fn prey_advantage_when_better_camouflaged() {
let m = TraitMatchup::new(ArmsRaceTrait::Detection, 0.4, 0.7);
assert!((m.prey_advantage() - 0.3).abs() < f32::EPSILON);
}
#[test]
fn trait_pressure_increases_with_deficit() {
let low = trait_pressure(0.7, 0.8, 0.5);
let high = trait_pressure(0.3, 0.8, 0.5);
assert!(high > low, "bigger deficit = more pressure");
}
#[test]
fn no_pressure_when_ahead() {
let pressure = trait_pressure(0.9, 0.5, 1.0);
assert_eq!(pressure, 0.0);
}
#[test]
fn red_queen_stasis() {
let balance = red_queen_balance(0.05, 0.05);
assert!(balance.abs() < f32::EPSILON);
}
#[test]
fn red_queen_predator_gaining() {
assert!(red_queen_balance(0.08, 0.03) > 0.0);
}
#[test]
fn encounter_rate_proportional() {
let r1 = encounter_rate(10.0, 50.0, 0.5);
let r2 = encounter_rate(20.0, 50.0, 0.5);
assert!((r2 - r1 * 2.0).abs() < f32::EPSILON);
}
#[test]
fn encounter_rate_zero_density() {
assert_eq!(encounter_rate(0.0, 50.0, 0.5), 0.0);
assert_eq!(encounter_rate(10.0, 0.0, 0.5), 0.0);
}
#[test]
fn holling_type2_saturates() {
let low_prey = functional_response_type2(1.0, 0.5, 0.2);
let high_prey = functional_response_type2(100.0, 0.5, 0.2);
assert!(high_prey > low_prey);
assert!(high_prey < 5.0 + f32::EPSILON);
}
#[test]
fn holling_type2_zero_safe() {
assert_eq!(functional_response_type2(0.0, 0.5, 0.2), 0.0);
assert_eq!(functional_response_type2(10.0, 0.0, 0.2), 0.0);
}
#[test]
fn serde_roundtrip_ecological_role() {
for r in [
EcologicalRole::ApexPredator,
EcologicalRole::Mesopredator,
EcologicalRole::Prey,
EcologicalRole::Parasite,
] {
let json = serde_json::to_string(&r).unwrap();
let r2: EcologicalRole = serde_json::from_str(&json).unwrap();
assert_eq!(r, r2);
}
}
#[test]
fn serde_roundtrip_arms_race_trait() {
for t in [
ArmsRaceTrait::Speed,
ArmsRaceTrait::Detection,
ArmsRaceTrait::Toxicity,
ArmsRaceTrait::Armor,
ArmsRaceTrait::Endurance,
ArmsRaceTrait::GroupDefense,
] {
let json = serde_json::to_string(&t).unwrap();
let t2: ArmsRaceTrait = serde_json::from_str(&json).unwrap();
assert_eq!(t, t2);
}
}
#[test]
fn serde_roundtrip_trait_matchup() {
let m = TraitMatchup::new(ArmsRaceTrait::Speed, 0.7, 0.6);
let json = serde_json::to_string(&m).unwrap();
let m2: TraitMatchup = serde_json::from_str(&json).unwrap();
assert!((m.predator_trait - m2.predator_trait).abs() < f32::EPSILON);
assert_eq!(m.trait_type, m2.trait_type);
}
}