use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PayoffMatrix {
pub payoffs: [[(f64, f64); 2]; 2],
}
impl PayoffMatrix {
#[inline]
#[must_use]
pub fn new(payoffs: [[(f64, f64); 2]; 2]) -> Self {
Self { payoffs }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Strategy {
Cooperate,
Defect,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NashEquilibrium {
pub player1: Strategy,
pub player2: Strategy,
pub payoffs: (f64, f64),
}
impl NashEquilibrium {
#[inline]
#[must_use]
pub fn new(player1: Strategy, player2: Strategy, payoffs: (f64, f64)) -> Self {
Self {
player1,
player2,
payoffs,
}
}
}
#[inline]
#[must_use]
pub fn prisoners_dilemma() -> PayoffMatrix {
PayoffMatrix {
payoffs: [
[(3.0, 3.0), (0.0, 5.0)], [(5.0, 0.0), (1.0, 1.0)], ],
}
}
#[must_use = "returns Nash equilibria without side effects"]
pub fn find_nash_equilibria(matrix: &PayoffMatrix) -> Vec<NashEquilibrium> {
let mut equilibria = Vec::new();
let strategies = [Strategy::Cooperate, Strategy::Defect];
for i in 0..2 {
for j in 0..2 {
let (p1, p2) = matrix.payoffs[i][j];
let other_i = 1 - i;
let other_j = 1 - j;
let p1_alt = matrix.payoffs[other_i][j].0;
let p2_alt = matrix.payoffs[i][other_j].1;
if p1 >= p1_alt && p2 >= p2_alt {
equilibria.push(NashEquilibrium {
player1: strategies[i],
player2: strategies[j],
payoffs: (p1, p2),
});
}
}
}
equilibria
}
#[must_use = "returns the scores without side effects"]
pub fn iterated_prisoners_dilemma<F1, F2>(
mut strategy1: F1,
mut strategy2: F2,
rounds: usize,
) -> (f64, f64)
where
F1: FnMut(Option<Strategy>) -> Strategy,
F2: FnMut(Option<Strategy>) -> Strategy,
{
let pd = prisoners_dilemma();
let mut score1 = 0.0;
let mut score2 = 0.0;
let mut prev1 = None;
let mut prev2 = None;
for _ in 0..rounds {
let move1 = strategy1(prev2);
let move2 = strategy2(prev1);
let i = match move1 {
Strategy::Cooperate => 0,
Strategy::Defect => 1,
};
let j = match move2 {
Strategy::Cooperate => 0,
Strategy::Defect => 1,
};
let (p1, p2) = pd.payoffs[i][j];
score1 += p1;
score2 += p2;
prev1 = Some(move1);
prev2 = Some(move2);
}
(score1, score2)
}
#[inline]
#[must_use]
pub fn tit_for_tat(opponent_last: Option<Strategy>) -> Strategy {
opponent_last.unwrap_or(Strategy::Cooperate)
}
#[inline]
#[must_use]
pub fn always_cooperate(_opponent_last: Option<Strategy>) -> Strategy {
Strategy::Cooperate
}
#[inline]
#[must_use]
pub fn always_defect(_opponent_last: Option<Strategy>) -> Strategy {
Strategy::Defect
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prisoners_dilemma_payoffs() {
let pd = prisoners_dilemma();
assert_eq!(pd.payoffs[0][0], (3.0, 3.0)); assert_eq!(pd.payoffs[0][1], (0.0, 5.0)); assert_eq!(pd.payoffs[1][0], (5.0, 0.0)); assert_eq!(pd.payoffs[1][1], (1.0, 1.0)); }
#[test]
fn test_nash_equilibrium_pd() {
let pd = prisoners_dilemma();
let equilibria = find_nash_equilibria(&pd);
assert_eq!(equilibria.len(), 1);
assert_eq!(equilibria[0].player1, Strategy::Defect);
assert_eq!(equilibria[0].player2, Strategy::Defect);
assert_eq!(equilibria[0].payoffs, (1.0, 1.0));
}
#[test]
fn test_tit_for_tat_first_move() {
assert_eq!(tit_for_tat(None), Strategy::Cooperate);
}
#[test]
fn test_tit_for_tat_copies() {
assert_eq!(tit_for_tat(Some(Strategy::Defect)), Strategy::Defect);
assert_eq!(tit_for_tat(Some(Strategy::Cooperate)), Strategy::Cooperate);
}
#[test]
fn test_iterated_pd_tft_vs_always_cooperate() {
let (s1, s2) = iterated_prisoners_dilemma(tit_for_tat, always_cooperate, 10);
assert!((s1 - 30.0).abs() < 1e-10);
assert!((s2 - 30.0).abs() < 1e-10);
}
#[test]
fn test_iterated_pd_always_defect_dominates() {
let (s1, s2) = iterated_prisoners_dilemma(always_defect, always_cooperate, 10);
assert!((s1 - 50.0).abs() < 1e-10);
assert!((s2 - 0.0).abs() < 1e-10);
}
#[test]
fn test_payoff_matrix_serde_roundtrip() {
let pd = prisoners_dilemma();
let json = serde_json::to_string(&pd).unwrap();
let back: PayoffMatrix = serde_json::from_str(&json).unwrap();
assert_eq!(pd.payoffs[0][0], back.payoffs[0][0]);
}
#[test]
fn test_strategy_serde_roundtrip() {
let s = Strategy::Cooperate;
let json = serde_json::to_string(&s).unwrap();
let back: Strategy = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn test_nash_equilibrium_serde_roundtrip() {
let ne = NashEquilibrium {
player1: Strategy::Defect,
player2: Strategy::Defect,
payoffs: (1.0, 1.0),
};
let json = serde_json::to_string(&ne).unwrap();
let back: NashEquilibrium = serde_json::from_str(&json).unwrap();
assert_eq!(ne.player1, back.player1);
}
#[test]
fn test_coordination_game_two_equilibria() {
let matrix = PayoffMatrix::new([[(2.0, 2.0), (0.0, 0.0)], [(0.0, 0.0), (1.0, 1.0)]]);
let eq = find_nash_equilibria(&matrix);
assert_eq!(eq.len(), 2);
}
#[test]
fn test_iterated_pd_zero_rounds() {
let (s1, s2) = iterated_prisoners_dilemma(always_cooperate, always_defect, 0);
assert!((s1 - 0.0).abs() < 1e-10);
assert!((s2 - 0.0).abs() < 1e-10);
}
#[test]
fn test_tit_for_tat_vs_always_defect() {
let (s1, s2) = iterated_prisoners_dilemma(tit_for_tat, always_defect, 10);
assert!((s1 - 9.0).abs() < 1e-10); assert!((s2 - 14.0).abs() < 1e-10); }
#[test]
fn test_payoff_matrix_new() {
let pm = PayoffMatrix::new([[(1.0, 2.0), (3.0, 4.0)], [(5.0, 6.0), (7.0, 8.0)]]);
assert_eq!(pm.payoffs[0][0], (1.0, 2.0));
assert_eq!(pm.payoffs[1][1], (7.0, 8.0));
}
#[test]
fn test_nash_equilibrium_new() {
let ne = NashEquilibrium::new(Strategy::Cooperate, Strategy::Defect, (0.0, 5.0));
assert_eq!(ne.player1, Strategy::Cooperate);
assert_eq!(ne.player2, Strategy::Defect);
assert_eq!(ne.payoffs, (0.0, 5.0));
}
}