use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ContestStrategy {
Hawk,
Dove,
Bourgeois,
}
#[must_use]
pub fn hawk_dove_ess(resource_value: f32, contest_cost: f32) -> f32 {
if contest_cost <= 0.0 || resource_value >= contest_cost {
return 1.0;
}
(resource_value / contest_cost).clamp(0.0, 1.0)
}
#[must_use]
pub fn hawk_dove_payoff(
is_hawk: bool,
opponent_hawk: bool,
resource_value: f32,
contest_cost: f32,
) -> f32 {
match (is_hawk, opponent_hawk) {
(true, true) => (resource_value - contest_cost) * 0.5, (true, false) => resource_value, (false, true) => 0.0, (false, false) => resource_value * 0.5, }
}
#[must_use]
pub fn bourgeois_payoff(is_owner: bool, resource_value: f32, _contest_cost: f32) -> f32 {
if is_owner { resource_value } else { 0.0 }
}
#[must_use]
pub fn war_of_attrition_duration(
resource_value: f32,
own_strength: f32,
opponent_strength: f32,
) -> f32 {
let own_strength = own_strength.clamp(0.0, 1.0);
let opponent_strength = opponent_strength.clamp(0.0, 1.0);
let min_persistence = own_strength.min(opponent_strength);
resource_value * min_persistence
}
#[must_use]
pub fn producer_payoff(finder_share: f32, group_size: u32, scrounger_fraction: f32) -> f32 {
let finder_share = finder_share.clamp(0.0, 1.0);
let scrounger_fraction = scrounger_fraction.clamp(0.0, 1.0);
let group_size = group_size.max(1);
let n_scroungers = (group_size as f32 * scrounger_fraction).max(0.0);
let scrounger_tax = (1.0 - finder_share) * (n_scroungers / (n_scroungers + 1.0));
(1.0 - scrounger_tax).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hawk_dove_ess_cheap_resource() {
let freq = hawk_dove_ess(2.0, 10.0);
assert!((freq - 0.2).abs() < f32::EPSILON);
}
#[test]
fn hawk_dove_ess_expensive_resource() {
assert_eq!(hawk_dove_ess(10.0, 5.0), 1.0);
}
#[test]
fn hawk_dove_ess_zero_cost() {
assert_eq!(hawk_dove_ess(5.0, 0.0), 1.0);
}
#[test]
fn hawk_v_hawk_costly() {
let payoff = hawk_dove_payoff(true, true, 10.0, 20.0);
assert!((payoff - (-5.0)).abs() < f32::EPSILON);
}
#[test]
fn hawk_v_dove_wins() {
assert!((hawk_dove_payoff(true, false, 10.0, 20.0) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn dove_v_dove_splits() {
assert!((hawk_dove_payoff(false, false, 10.0, 20.0) - 5.0).abs() < f32::EPSILON);
}
#[test]
fn bourgeois_owner_wins() {
assert!((bourgeois_payoff(true, 10.0, 20.0) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn bourgeois_intruder_retreats() {
assert_eq!(bourgeois_payoff(false, 10.0, 20.0), 0.0);
}
#[test]
fn war_even_longer_than_mismatch() {
let even = war_of_attrition_duration(10.0, 0.5, 0.5);
let mismatch = war_of_attrition_duration(10.0, 0.9, 0.2);
assert!(even > mismatch);
}
#[test]
fn war_zero_strength_instant() {
assert_eq!(war_of_attrition_duration(10.0, 0.0, 0.5), 0.0);
}
#[test]
fn producer_no_scroungers_keeps_all() {
let payoff = producer_payoff(0.6, 5, 0.0);
assert!((payoff - 1.0).abs() < f32::EPSILON);
}
#[test]
fn producer_more_scroungers_less_payoff() {
let few = producer_payoff(0.6, 10, 0.2);
let many = producer_payoff(0.6, 10, 0.8);
assert!(few > many);
}
#[test]
fn serde_roundtrip_contest_strategy() {
for s in [
ContestStrategy::Hawk,
ContestStrategy::Dove,
ContestStrategy::Bourgeois,
] {
let json = serde_json::to_string(&s).unwrap();
let s2: ContestStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(s, s2);
}
}
}