use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MatingSystem {
Monogamous,
Polygynous,
Polyandrous,
Promiscuous,
Lek,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CourtshipPhase {
Searching,
Displaying,
Courting,
Assessment,
Bonded,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FitnessTraits {
pub condition: f32,
pub display_quality: f32,
pub territory_quality: f32,
pub genetic_quality: f32,
pub vigor: f32,
}
impl FitnessTraits {
#[must_use]
pub fn attractiveness(&self) -> f32 {
let score = self.display_quality * 0.30
+ self.condition * 0.25
+ self.territory_quality * 0.20
+ self.genetic_quality * 0.15
+ self.vigor * 0.10;
score.clamp(0.0, 1.0)
}
}
#[must_use]
pub fn mate_acceptance(candidate: &FitnessTraits, chooser_threshold: f32, competition: u32) -> f32 {
let attractiveness = candidate.attractiveness();
let threshold = chooser_threshold.clamp(0.0, 1.0);
let competition_factor = 1.0 + (competition as f32).ln_1p() * 0.15;
let effective_threshold = (threshold * competition_factor).clamp(0.0, 1.0);
if attractiveness < effective_threshold {
return 0.0;
}
let excess = attractiveness - effective_threshold;
let max_excess = 1.0 - effective_threshold;
if max_excess <= 0.0 {
return 1.0;
}
(excess / max_excess).clamp(0.0, 1.0)
}
#[must_use]
pub fn display_cost(display_intensity: f32, body_condition: f32) -> f32 {
let display_intensity = display_intensity.clamp(0.0, 1.0);
let body_condition = body_condition.clamp(0.0, 1.0);
let base_cost = display_intensity * display_intensity; let condition_penalty = 1.0 + (1.0 - body_condition) * 0.5;
(base_cost * condition_penalty).clamp(0.0, 1.0)
}
#[must_use]
pub fn selection_pressure(operational_sex_ratio: f32) -> f32 {
if operational_sex_ratio <= 0.0 {
return 1.0;
}
1.0 + operational_sex_ratio.ln_1p() * 0.5
}
#[cfg(test)]
mod tests {
use super::*;
fn good_mate() -> FitnessTraits {
FitnessTraits {
condition: 0.9,
display_quality: 0.85,
territory_quality: 0.8,
genetic_quality: 0.7,
vigor: 0.8,
}
}
fn poor_mate() -> FitnessTraits {
FitnessTraits {
condition: 0.2,
display_quality: 0.1,
territory_quality: 0.1,
genetic_quality: 0.3,
vigor: 0.2,
}
}
#[test]
fn good_mate_more_attractive() {
assert!(good_mate().attractiveness() > poor_mate().attractiveness());
}
#[test]
fn attractiveness_bounded() {
let a = good_mate().attractiveness();
assert!((0.0..=1.0).contains(&a));
}
#[test]
fn high_quality_accepted() {
let prob = mate_acceptance(&good_mate(), 0.3, 2);
assert!(prob > 0.5, "good mate should be accepted: {prob}");
}
#[test]
fn low_quality_rejected() {
let prob = mate_acceptance(&poor_mate(), 0.5, 5);
assert_eq!(prob, 0.0, "poor mate should be rejected");
}
#[test]
fn more_competition_pickier() {
let few = mate_acceptance(&good_mate(), 0.5, 1);
let many = mate_acceptance(&good_mate(), 0.5, 20);
assert!(
few >= many,
"more competition should not increase acceptance: few={few}, many={many}"
);
}
#[test]
fn display_cost_increases_with_intensity() {
let low = display_cost(0.2, 0.8);
let high = display_cost(0.9, 0.8);
assert!(high > low);
}
#[test]
fn poor_condition_costlier_display() {
let healthy = display_cost(0.7, 0.9);
let sickly = display_cost(0.7, 0.2);
assert!(sickly > healthy);
}
#[test]
fn balanced_sex_ratio_low_pressure() {
let balanced = selection_pressure(1.0);
let skewed = selection_pressure(5.0);
assert!(skewed > balanced);
}
#[test]
fn zero_ratio_baseline() {
assert!((selection_pressure(0.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_mating_system() {
for s in [
MatingSystem::Monogamous,
MatingSystem::Polygynous,
MatingSystem::Polyandrous,
MatingSystem::Promiscuous,
MatingSystem::Lek,
] {
let json = serde_json::to_string(&s).unwrap();
let s2: MatingSystem = serde_json::from_str(&json).unwrap();
assert_eq!(s, s2);
}
}
#[test]
fn serde_roundtrip_courtship_phase() {
for p in [
CourtshipPhase::Searching,
CourtshipPhase::Displaying,
CourtshipPhase::Courting,
CourtshipPhase::Assessment,
CourtshipPhase::Bonded,
CourtshipPhase::Rejected,
] {
let json = serde_json::to_string(&p).unwrap();
let p2: CourtshipPhase = serde_json::from_str(&json).unwrap();
assert_eq!(p, p2);
}
}
#[test]
fn serde_roundtrip_fitness_traits() {
let t = good_mate();
let json = serde_json::to_string(&t).unwrap();
let t2: FitnessTraits = serde_json::from_str(&json).unwrap();
assert!((t.condition - t2.condition).abs() < f32::EPSILON);
assert!((t.display_quality - t2.display_quality).abs() < f32::EPSILON);
}
}