use scirs2_core::ndarray::{Array2, ArrayView2};
use crate::error::OptimizeError;
#[derive(Debug, Clone)]
pub struct NormalFormGame {
pub payoff_matrix_1: Array2<f64>,
pub payoff_matrix_2: Array2<f64>,
pub n_strategies_1: usize,
pub n_strategies_2: usize,
}
impl NormalFormGame {
pub fn new(payoff_1: Array2<f64>, payoff_2: Array2<f64>) -> Result<Self, OptimizeError> {
if payoff_1.shape() != payoff_2.shape() {
return Err(OptimizeError::ValueError(format!(
"Payoff matrix shapes must match: {:?} != {:?}",
payoff_1.shape(),
payoff_2.shape()
)));
}
let n1 = payoff_1.nrows();
let n2 = payoff_1.ncols();
if n1 == 0 || n2 == 0 {
return Err(OptimizeError::ValueError(
"Payoff matrices must be non-empty".to_string(),
));
}
Ok(Self {
n_strategies_1: n1,
n_strategies_2: n2,
payoff_matrix_1: payoff_1,
payoff_matrix_2: payoff_2,
})
}
pub fn zero_sum(payoff: Array2<f64>) -> Self {
let n1 = payoff.nrows();
let n2 = payoff.ncols();
let payoff_2 = -payoff.clone();
Self {
n_strategies_1: n1,
n_strategies_2: n2,
payoff_matrix_1: payoff,
payoff_matrix_2: payoff_2,
}
}
pub fn symmetric(payoff: Array2<f64>) -> Result<Self, OptimizeError> {
let n1 = payoff.nrows();
let n2 = payoff.ncols();
if n1 != n2 {
return Err(OptimizeError::ValueError(format!(
"Symmetric game requires a square payoff matrix, got {}×{}",
n1, n2
)));
}
let payoff_2 = payoff.t().to_owned();
Ok(Self {
n_strategies_1: n1,
n_strategies_2: n2,
payoff_matrix_1: payoff,
payoff_matrix_2: payoff_2,
})
}
pub fn payoff(&self, s1: usize, s2: usize) -> (f64, f64) {
(self.payoff_matrix_1[[s1, s2]], self.payoff_matrix_2[[s1, s2]])
}
pub fn expected_payoff(
&self,
mixed_1: &[f64],
mixed_2: &[f64],
) -> Result<(f64, f64), OptimizeError> {
if mixed_1.len() != self.n_strategies_1 {
return Err(OptimizeError::ValueError(format!(
"mixed_1 length {} != n_strategies_1 {}",
mixed_1.len(),
self.n_strategies_1
)));
}
if mixed_2.len() != self.n_strategies_2 {
return Err(OptimizeError::ValueError(format!(
"mixed_2 length {} != n_strategies_2 {}",
mixed_2.len(),
self.n_strategies_2
)));
}
let tol = 1e-9;
let sum1: f64 = mixed_1.iter().sum();
if (sum1 - 1.0).abs() > tol {
return Err(OptimizeError::ValueError(format!(
"mixed_1 must sum to 1.0, got {}",
sum1
)));
}
let sum2: f64 = mixed_2.iter().sum();
if (sum2 - 1.0).abs() > tol {
return Err(OptimizeError::ValueError(format!(
"mixed_2 must sum to 1.0, got {}",
sum2
)));
}
let mut ep1 = 0.0_f64;
let mut ep2 = 0.0_f64;
for i in 0..self.n_strategies_1 {
for j in 0..self.n_strategies_2 {
let prob = mixed_1[i] * mixed_2[j];
ep1 += prob * self.payoff_matrix_1[[i, j]];
ep2 += prob * self.payoff_matrix_2[[i, j]];
}
}
Ok((ep1, ep2))
}
}
#[derive(Debug, Clone)]
pub struct NashEquilibrium {
pub strategy_1: Vec<f64>,
pub strategy_2: Vec<f64>,
pub payoff_1: f64,
pub payoff_2: f64,
pub is_pure: bool,
}
pub fn find_pure_nash_equilibria(game: &NormalFormGame) -> Vec<NashEquilibrium> {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
let mut results = Vec::new();
for i in 0..n1 {
for j in 0..n2 {
let p1_val = game.payoff_matrix_1[[i, j]];
let p1_br = (0..n1).all(|i2| game.payoff_matrix_1[[i2, j]] <= p1_val + 1e-12);
let p2_val = game.payoff_matrix_2[[i, j]];
let p2_br = (0..n2).all(|j2| game.payoff_matrix_2[[i, j2]] <= p2_val + 1e-12);
if p1_br && p2_br {
let mut s1 = vec![0.0; n1];
let mut s2 = vec![0.0; n2];
s1[i] = 1.0;
s2[j] = 1.0;
results.push(NashEquilibrium {
strategy_1: s1,
strategy_2: s2,
payoff_1: p1_val,
payoff_2: p2_val,
is_pure: true,
});
}
}
}
results
}
pub fn find_all_nash_equilibria(
game: &NormalFormGame,
tol: f64,
) -> Result<Vec<NashEquilibrium>, OptimizeError> {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
let mut results = find_pure_nash_equilibria(game);
for supp1_mask in 1u64..(1u64 << n1) {
let supp1: Vec<usize> = (0..n1).filter(|&i| (supp1_mask >> i) & 1 == 1).collect();
for supp2_mask in 1u64..(1u64 << n2) {
let supp2: Vec<usize> = (0..n2).filter(|&j| (supp2_mask >> j) & 1 == 1).collect();
let k1 = supp1.len();
let k2 = supp2.len();
if k1 == 1 && k2 == 1 {
continue;
}
if let Some(ne) = solve_support_ne(game, &supp1, &supp2, tol) {
let is_duplicate = results.iter().any(|existing| {
strategies_approx_equal(&existing.strategy_1, &ne.strategy_1, tol)
&& strategies_approx_equal(&existing.strategy_2, &ne.strategy_2, tol)
});
if !is_duplicate {
results.push(ne);
}
}
}
}
Ok(results)
}
fn solve_support_ne(
game: &NormalFormGame,
supp1: &[usize],
supp2: &[usize],
tol: f64,
) -> Option<NashEquilibrium> {
let k1 = supp1.len();
let k2 = supp2.len();
let q = solve_indifference(
&game.payoff_matrix_1,
supp1,
supp2,
tol,
)?;
let p = solve_indifference(
&game.payoff_matrix_2.t().to_owned(),
supp2,
supp1,
tol,
)?;
let v1: f64 = supp1.iter().enumerate().map(|(idx, &i)| {
supp2.iter().enumerate().map(|(jdx, &j)| {
game.payoff_matrix_1[[i, j]] * q[jdx]
}).sum::<f64>() * p[idx]
}).sum();
for i in 0..game.n_strategies_1 {
if !supp1.contains(&i) {
let dev_payoff: f64 = supp2.iter().enumerate()
.map(|(jdx, &j)| game.payoff_matrix_1[[i, j]] * q[jdx])
.sum();
if dev_payoff > v1 + tol {
return None;
}
}
}
let v2: f64 = supp2.iter().enumerate().map(|(jdx, &j)| {
supp1.iter().enumerate().map(|(idx, &i)| {
game.payoff_matrix_2[[i, j]] * p[idx]
}).sum::<f64>() * q[jdx]
}).sum();
for j in 0..game.n_strategies_2 {
if !supp2.contains(&j) {
let dev_payoff: f64 = supp1.iter().enumerate()
.map(|(idx, &i)| game.payoff_matrix_2[[i, j]] * p[idx])
.sum();
if dev_payoff > v2 + tol {
return None;
}
}
}
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
let mut strategy_1 = vec![0.0; n1];
let mut strategy_2 = vec![0.0; n2];
for (idx, &i) in supp1.iter().enumerate() {
strategy_1[i] = p[idx];
}
for (jdx, &j) in supp2.iter().enumerate() {
strategy_2[j] = q[jdx];
}
let is_pure = k1 == 1 && k2 == 1;
Some(NashEquilibrium {
strategy_1,
strategy_2,
payoff_1: v1,
payoff_2: v2,
is_pure,
})
}
fn solve_indifference(
payoff: &Array2<f64>,
supp_row: &[usize],
supp_col: &[usize],
tol: f64,
) -> Option<Vec<f64>> {
let k_col = supp_col.len();
let k_row = supp_row.len();
if k_col == 1 {
return Some(vec![1.0]);
}
if k_row != k_col {
}
let n_eq = k_row; let n_var = k_col;
let mut mat = vec![0.0_f64; n_eq * n_var];
let mut rhs = vec![0.0_f64; n_eq];
for eq_idx in 0..(k_row - 1) {
let i0 = supp_row[0];
let i1 = supp_row[eq_idx + 1];
for (col_idx, &j) in supp_col.iter().enumerate() {
mat[eq_idx * n_var + col_idx] = payoff[[i0, j]] - payoff[[i1, j]];
}
rhs[eq_idx] = 0.0;
}
let simplex_row = k_row - 1;
for col_idx in 0..k_col {
mat[simplex_row * n_var + col_idx] = 1.0;
}
rhs[simplex_row] = 1.0;
let q = if n_eq == n_var {
gaussian_elimination(&mat, &rhs, n_var)?
} else {
least_squares_solve(&mat, &rhs, n_eq, n_var)?
};
if q.iter().any(|&v| v < -tol) {
return None;
}
let s: f64 = q.iter().sum();
if (s - 1.0).abs() > tol * 10.0 {
return None;
}
let q_clamped: Vec<f64> = q.iter().map(|&v| v.max(0.0)).collect();
let s2: f64 = q_clamped.iter().sum();
if s2 < tol {
return None;
}
let q_normalized: Vec<f64> = q_clamped.iter().map(|&v| v / s2).collect();
if q_normalized.iter().any(|&v| v < tol) {
return None;
}
Some(q_normalized)
}
fn gaussian_elimination(mat: &[f64], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
let mut a = mat.to_vec();
let mut b = rhs.to_vec();
for col in 0..n {
let pivot_row = (col..n).max_by(|&r1, &r2| {
a[r1 * n + col].abs().partial_cmp(&a[r2 * n + col].abs())
.unwrap_or(std::cmp::Ordering::Equal)
})?;
if a[pivot_row * n + col].abs() < 1e-14 {
return None;
}
if pivot_row != col {
for k in 0..n {
a.swap(col * n + k, pivot_row * n + k);
}
b.swap(col, pivot_row);
}
let pivot = a[col * n + col];
for row in (col + 1)..n {
let factor = a[row * n + col] / pivot;
for k in col..n {
let val = a[col * n + k] * factor;
a[row * n + k] -= val;
}
b[row] -= b[col] * factor;
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut sum = b[i];
for j in (i + 1)..n {
sum -= a[i * n + j] * x[j];
}
if a[i * n + i].abs() < 1e-14 {
return None;
}
x[i] = sum / a[i * n + i];
}
Some(x)
}
fn least_squares_solve(mat: &[f64], rhs: &[f64], n_eq: usize, n_var: usize) -> Option<Vec<f64>> {
let mut ata = vec![0.0_f64; n_var * n_var];
for i in 0..n_var {
for j in 0..n_var {
for k in 0..n_eq {
ata[i * n_var + j] += mat[k * n_var + i] * mat[k * n_var + j];
}
}
}
let mut atb = vec![0.0_f64; n_var];
for i in 0..n_var {
for k in 0..n_eq {
atb[i] += mat[k * n_var + i] * rhs[k];
}
}
gaussian_elimination(&ata, &atb, n_var)
}
fn strategies_approx_equal(a: &[f64], b: &[f64], tol: f64) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < tol * 10.0)
}
pub fn best_response_1(game: &NormalFormGame, opponent_mixed: &[f64]) -> Vec<usize> {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
if opponent_mixed.len() != n2 {
return Vec::new();
}
let payoffs: Vec<f64> = (0..n1)
.map(|i| {
(0..n2)
.map(|j| game.payoff_matrix_1[[i, j]] * opponent_mixed[j])
.sum::<f64>()
})
.collect();
let max_payoff = payoffs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(0..n1)
.filter(|&i| (payoffs[i] - max_payoff).abs() < 1e-9)
.collect()
}
pub fn best_response_2(game: &NormalFormGame, opponent_mixed: &[f64]) -> Vec<usize> {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
if opponent_mixed.len() != n1 {
return Vec::new();
}
let payoffs: Vec<f64> = (0..n2)
.map(|j| {
(0..n1)
.map(|i| game.payoff_matrix_2[[i, j]] * opponent_mixed[i])
.sum::<f64>()
})
.collect();
let max_payoff = payoffs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(0..n2)
.filter(|&j| (payoffs[j] - max_payoff).abs() < 1e-9)
.collect()
}
pub fn iterated_elimination(game: &mut NormalFormGame) -> (Vec<usize>, Vec<usize>) {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
let mut active_1: Vec<usize> = (0..n1).collect();
let mut active_2: Vec<usize> = (0..n2).collect();
let mut changed = true;
while changed {
changed = false;
let mut to_remove_1: Vec<usize> = Vec::new();
for &i in &active_1 {
if is_strictly_dominated_1(
&game.payoff_matrix_1,
i,
&active_1,
&active_2,
) {
to_remove_1.push(i);
changed = true;
}
}
active_1.retain(|i| !to_remove_1.contains(i));
let mut to_remove_2: Vec<usize> = Vec::new();
for &j in &active_2 {
if is_strictly_dominated_2(
&game.payoff_matrix_2,
j,
&active_1,
&active_2,
) {
to_remove_2.push(j);
changed = true;
}
}
active_2.retain(|j| !to_remove_2.contains(j));
}
(active_1, active_2)
}
fn is_strictly_dominated_1(
payoff: &Array2<f64>,
i: usize,
active_1: &[usize],
active_2: &[usize],
) -> bool {
let k1 = active_1.len();
for &i2 in active_1 {
if i2 == i {
continue;
}
let dominates = active_2
.iter()
.all(|&j| payoff[[i2, j]] > payoff[[i, j]]);
if dominates {
return true;
}
}
if k1 > 1 {
let alpha = 1.0 / (k1 - 1) as f64;
for &j in active_2 {
let mixed_val: f64 = active_1
.iter()
.filter(|&&i2| i2 != i)
.map(|&i2| payoff[[i2, j]] * alpha)
.sum();
if mixed_val <= payoff[[i, j]] {
return false;
}
}
if active_1.len() > 1 {
let all_dominated = active_2.iter().all(|&j| {
let mixed_val: f64 = active_1
.iter()
.filter(|&&i2| i2 != i)
.map(|&i2| payoff[[i2, j]] * alpha)
.sum();
mixed_val > payoff[[i, j]]
});
if all_dominated {
return true;
}
}
}
false
}
fn is_strictly_dominated_2(
payoff: &Array2<f64>,
j: usize,
active_1: &[usize],
active_2: &[usize],
) -> bool {
let k2 = active_2.len();
for &j2 in active_2 {
if j2 == j {
continue;
}
let dominates = active_1
.iter()
.all(|&i| payoff[[i, j2]] > payoff[[i, j]]);
if dominates {
return true;
}
}
if k2 > 1 {
let alpha = 1.0 / (k2 - 1) as f64;
let all_dominated = active_1.iter().all(|&i| {
let mixed_val: f64 = active_2
.iter()
.filter(|&&j2| j2 != j)
.map(|&j2| payoff[[i, j2]] * alpha)
.sum();
mixed_val > payoff[[i, j]]
});
if all_dominated {
return true;
}
}
false
}
pub fn dominant_strategy_equilibrium(game: &NormalFormGame) -> Option<(usize, usize)> {
let n1 = game.n_strategies_1;
let n2 = game.n_strategies_2;
let ds1 = (0..n1).find(|&i| {
(0..n1)
.filter(|&i2| i2 != i)
.all(|i2| (0..n2).all(|j| game.payoff_matrix_1[[i, j]] > game.payoff_matrix_1[[i2, j]]))
});
let ds2 = (0..n2).find(|&j| {
(0..n2)
.filter(|&j2| j2 != j)
.all(|j2| (0..n1).all(|i| game.payoff_matrix_2[[i, j]] > game.payoff_matrix_2[[i, j2]]))
});
match (ds1, ds2) {
(Some(i), Some(j)) => Some((i, j)),
_ => None,
}
}
pub fn replicator_dynamics(
payoff_matrix: ArrayView2<f64>,
initial_population: &[f64],
dt: f64,
n_steps: usize,
) -> Vec<Vec<f64>> {
let n = payoff_matrix.nrows();
if n == 0 || initial_population.len() != n {
return Vec::new();
}
let mut x = initial_population.to_vec();
let s: f64 = x.iter().sum();
if s > 0.0 {
x.iter_mut().for_each(|v| *v /= s);
}
let mut trajectory = vec![x.clone()];
for _ in 0..n_steps {
let ax: Vec<f64> = (0..n)
.map(|i| (0..n).map(|j| payoff_matrix[[i, j]] * x[j]).sum::<f64>())
.collect();
let mean_fitness: f64 = x.iter().zip(ax.iter()).map(|(xi, axi)| xi * axi).sum();
let mut x_new: Vec<f64> = x
.iter()
.zip(ax.iter())
.map(|(&xi, &axi)| xi + dt * xi * (axi - mean_fitness))
.collect();
x_new.iter_mut().for_each(|v| {
if *v < 0.0 {
*v = 0.0;
}
});
let total: f64 = x_new.iter().sum();
if total > 1e-15 {
x_new.iter_mut().for_each(|v| *v /= total);
}
x = x_new.clone();
trajectory.push(x_new);
}
trajectory
}
pub fn find_ess(payoff_matrix: ArrayView2<f64>) -> Vec<Vec<f64>> {
let n = payoff_matrix.nrows();
if n == 0 {
return Vec::new();
}
let mut ess_list = Vec::new();
for i in 0..n {
let mut p = vec![0.0; n];
p[i] = 1.0;
if is_ess_pure(payoff_matrix, i, n) {
ess_list.push(p);
}
}
let fixed_points = find_interior_ess_candidates(payoff_matrix, n);
for fp in fixed_points {
if !ess_list.iter().any(|e: &Vec<f64>| {
e.iter().zip(fp.iter()).all(|(a, b)| (a - b).abs() < 1e-6)
}) {
ess_list.push(fp);
}
}
ess_list
}
fn is_ess_pure(payoff: ArrayView2<f64>, i: usize, n: usize) -> bool {
let u_ii = payoff[[i, i]];
for j in 0..n {
if j == i {
continue;
}
let u_ji = payoff[[j, i]];
if u_ji > u_ii + 1e-12 {
return false;
}
if (u_ji - u_ii).abs() < 1e-12 {
let u_ij = payoff[[i, j]];
let u_jj = payoff[[j, j]];
if u_jj >= u_ij + 1e-12 {
return false;
}
}
}
true
}
fn find_interior_ess_candidates(payoff: ArrayView2<f64>, n: usize) -> Vec<Vec<f64>> {
if n > 4 {
return Vec::new(); }
let mut candidates: Vec<Vec<f64>> = Vec::new();
let tol = 1e-6;
let grid_points = if n == 2 {
(1..10)
.map(|k| {
let p = k as f64 / 10.0;
vec![p, 1.0 - p]
})
.collect::<Vec<_>>()
} else if n == 3 {
let mut pts = Vec::new();
for a in 1..9 {
for b in 1..(10 - a) {
let c = 10 - a - b;
if c > 0 {
pts.push(vec![a as f64 / 10.0, b as f64 / 10.0, c as f64 / 10.0]);
}
}
}
pts
} else {
vec![
vec![0.25, 0.25, 0.25, 0.25],
vec![0.4, 0.2, 0.2, 0.2],
vec![0.1, 0.5, 0.3, 0.1],
]
};
for start in grid_points {
let traj = replicator_dynamics(payoff, &start, 0.01, 10000);
if let Some(final_state) = traj.last() {
let is_interior = final_state.iter().all(|&v| v > tol);
if is_interior {
let ax: Vec<f64> = (0..n)
.map(|i| (0..n).map(|j| payoff[[i, j]] * final_state[j]).sum::<f64>())
.collect();
let mean_f: f64 = final_state.iter().zip(ax.iter()).map(|(xi, axi)| xi * axi).sum();
let is_fp = final_state
.iter()
.zip(ax.iter())
.all(|(&xi, &axi)| (xi * (axi - mean_f)).abs() < 1e-5);
if is_fp {
let is_dup = candidates.iter().any(|c: &Vec<f64>| {
c.iter()
.zip(final_state.iter())
.all(|(a, b)| (a - b).abs() < 1e-5)
});
if !is_dup {
if verify_ess_stability(payoff, final_state, n, tol) {
candidates.push(final_state.clone());
}
}
}
}
}
}
candidates
}
fn verify_ess_stability(
payoff: ArrayView2<f64>,
p_star: &[f64],
n: usize,
tol: f64,
) -> bool {
let u_pp: f64 = (0..n).map(|i| {
(0..n).map(|j| p_star[i] * payoff[[i, j]] * p_star[j]).sum::<f64>()
}).sum();
for i in 0..n {
if p_star[i] < tol {
continue;
}
for j in 0..n {
if j == i || p_star[j] < tol {
continue;
}
let eps = 0.01;
let q: Vec<f64> = p_star.iter().enumerate().map(|(k, &pk)| {
if k == j { pk + eps * (1.0 - pk) }
else { pk * (1.0 - eps) }
}).collect();
let s: f64 = q.iter().sum();
let q: Vec<f64> = q.iter().map(|v| v / s).collect();
let u_qq: f64 = (0..n).map(|a| {
(0..n).map(|b| q[a] * payoff[[a, b]] * q[b]).sum::<f64>()
}).sum();
let u_pq: f64 = (0..n).map(|a| {
(0..n).map(|b| p_star[a] * payoff[[a, b]] * q[b]).sum::<f64>()
}).sum();
let u_qp: f64 = (0..n).map(|a| {
(0..n).map(|b| q[a] * payoff[[a, b]] * p_star[b]).sum::<f64>()
}).sum();
if u_qp > u_pp + tol {
return false;
}
if (u_qp - u_pp).abs() < tol && u_qq >= u_pq - tol {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
use approx::assert_relative_eq;
#[test]
fn test_normal_form_game_construction() {
let p1 = array![[3.0, 0.0], [5.0, 1.0]];
let p2 = array![[3.0, 5.0], [0.0, 1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid game");
assert_eq!(game.n_strategies_1, 2);
assert_eq!(game.n_strategies_2, 2);
}
#[test]
fn test_normal_form_shape_mismatch() {
let p1 = array![[1.0, 2.0]];
let p2 = array![[1.0], [2.0]];
assert!(NormalFormGame::new(p1, p2).is_err());
}
#[test]
fn test_zero_sum_constructor() {
let payoff = array![[1.0, -1.0], [-1.0, 1.0]];
let game = NormalFormGame::zero_sum(payoff);
assert_eq!(game.payoff_matrix_2[[0, 0]], -1.0);
assert_eq!(game.payoff_matrix_2[[0, 1]], 1.0);
}
#[test]
fn test_symmetric_game() {
let payoff = array![[0.0, -1.0, 1.0], [1.0, 0.0, -1.0], [-1.0, 1.0, 0.0]];
let game = NormalFormGame::symmetric(payoff).expect("valid symmetric game");
assert_eq!(game.payoff_matrix_2[[1, 0]], game.payoff_matrix_1[[0, 1]]);
}
#[test]
fn test_payoff_lookup() {
let p1 = array![[1.0, 2.0], [3.0, 4.0]];
let p2 = array![[5.0, 6.0], [7.0, 8.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let (a, b) = game.payoff(1, 0);
assert_relative_eq!(a, 3.0);
assert_relative_eq!(b, 7.0);
}
#[test]
fn test_expected_payoff() {
let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let (ep1, ep2) = game
.expected_payoff(&[0.5, 0.5], &[0.5, 0.5])
.expect("valid mixed strategies");
assert_relative_eq!(ep1, 0.0, epsilon = 1e-10);
assert_relative_eq!(ep2, 0.0, epsilon = 1e-10);
}
#[test]
fn test_expected_payoff_invalid_length() {
let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
assert!(game.expected_payoff(&[1.0], &[0.5, 0.5]).is_err());
}
#[test]
fn test_prisoners_dilemma_pure_nash() {
let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let pure_ne = find_pure_nash_equilibria(&game);
assert_eq!(pure_ne.len(), 1);
assert_eq!(pure_ne[0].strategy_1, vec![0.0, 1.0]); assert_eq!(pure_ne[0].strategy_2, vec![0.0, 1.0]); assert!(pure_ne[0].is_pure);
}
#[test]
fn test_coordination_game_multiple_pure_nash() {
let p1 = array![[2.0, 0.0], [0.0, 1.0]];
let p2 = array![[2.0, 0.0], [0.0, 1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let pure_ne = find_pure_nash_equilibria(&game);
assert_eq!(pure_ne.len(), 2);
}
#[test]
fn test_matching_pennies_mixed_nash() {
let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let pure_ne = find_pure_nash_equilibria(&game);
assert_eq!(pure_ne.len(), 0);
let all_ne = find_all_nash_equilibria(&game, 1e-9).expect("success");
assert_eq!(all_ne.len(), 1);
assert_relative_eq!(all_ne[0].strategy_1[0], 0.5, epsilon = 1e-6);
assert_relative_eq!(all_ne[0].strategy_2[0], 0.5, epsilon = 1e-6);
}
#[test]
fn test_best_response_1() {
let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let br = best_response_1(&game, &[0.0, 1.0]);
assert!(br.contains(&1)); }
#[test]
fn test_best_response_2() {
let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let br = best_response_2(&game, &[0.0, 1.0]);
assert!(br.contains(&1)); }
#[test]
fn test_dominant_strategy_equilibrium() {
let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let ds = dominant_strategy_equilibrium(&game);
assert_eq!(ds, Some((1, 1)));
}
#[test]
fn test_dominant_strategy_equilibrium_none() {
let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
let game = NormalFormGame::new(p1, p2).expect("valid");
let ds = dominant_strategy_equilibrium(&game);
assert!(ds.is_none());
}
#[test]
fn test_iterated_elimination() {
let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
let mut game = NormalFormGame::new(p1, p2).expect("valid");
let (s1, s2) = iterated_elimination(&mut game);
assert_eq!(s1, vec![1]);
assert_eq!(s2, vec![1]);
}
#[test]
fn test_replicator_dynamics_convergence() {
let payoff = array![[0.0, 3.0], [1.0, 2.0]];
let initial = vec![0.5, 0.5];
let traj = replicator_dynamics(payoff.view(), &initial, 0.01, 1000);
assert!(!traj.is_empty());
let final_state = traj.last().expect("non-empty trajectory");
let total: f64 = final_state.iter().sum();
assert_relative_eq!(total, 1.0, epsilon = 1e-6);
}
#[test]
fn test_find_ess_pure() {
let payoff = array![[0.0, -1.0, 1.0], [1.0, 0.0, -1.0], [-1.0, 1.0, 0.0]];
let ess = find_ess(payoff.view());
let pure_ess: Vec<_> = ess.iter().filter(|e| e.iter().filter(|&&v| v > 0.01).count() == 1).collect();
assert_eq!(pure_ess.len(), 0);
}
#[test]
fn test_symmetric_game_invalid_shape() {
let payoff = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
assert!(NormalFormGame::symmetric(payoff).is_err());
}
}