1use scirs2_core::ndarray::{Array2, ArrayView2};
8
9use crate::error::OptimizeError;
10
11#[derive(Debug, Clone)]
17pub struct NormalFormGame {
18 pub payoff_matrix_1: Array2<f64>,
20 pub payoff_matrix_2: Array2<f64>,
22 pub n_strategies_1: usize,
24 pub n_strategies_2: usize,
26}
27
28impl NormalFormGame {
29 pub fn new(payoff_1: Array2<f64>, payoff_2: Array2<f64>) -> Result<Self, OptimizeError> {
38 if payoff_1.shape() != payoff_2.shape() {
39 return Err(OptimizeError::ValueError(format!(
40 "Payoff matrix shapes must match: {:?} != {:?}",
41 payoff_1.shape(),
42 payoff_2.shape()
43 )));
44 }
45 let n1 = payoff_1.nrows();
46 let n2 = payoff_1.ncols();
47 if n1 == 0 || n2 == 0 {
48 return Err(OptimizeError::ValueError(
49 "Payoff matrices must be non-empty".to_string(),
50 ));
51 }
52 Ok(Self {
53 n_strategies_1: n1,
54 n_strategies_2: n2,
55 payoff_matrix_1: payoff_1,
56 payoff_matrix_2: payoff_2,
57 })
58 }
59
60 pub fn zero_sum(payoff: Array2<f64>) -> Self {
64 let n1 = payoff.nrows();
65 let n2 = payoff.ncols();
66 let payoff_2 = -payoff.clone();
67 Self {
68 n_strategies_1: n1,
69 n_strategies_2: n2,
70 payoff_matrix_1: payoff,
71 payoff_matrix_2: payoff_2,
72 }
73 }
74
75 pub fn symmetric(payoff: Array2<f64>) -> Result<Self, OptimizeError> {
83 let n1 = payoff.nrows();
84 let n2 = payoff.ncols();
85 if n1 != n2 {
86 return Err(OptimizeError::ValueError(format!(
87 "Symmetric game requires a square payoff matrix, got {}×{}",
88 n1, n2
89 )));
90 }
91 let payoff_2 = payoff.t().to_owned();
92 Ok(Self {
93 n_strategies_1: n1,
94 n_strategies_2: n2,
95 payoff_matrix_1: payoff,
96 payoff_matrix_2: payoff_2,
97 })
98 }
99
100 pub fn payoff(&self, s1: usize, s2: usize) -> (f64, f64) {
105 (
106 self.payoff_matrix_1[[s1, s2]],
107 self.payoff_matrix_2[[s1, s2]],
108 )
109 }
110
111 pub fn expected_payoff(
121 &self,
122 mixed_1: &[f64],
123 mixed_2: &[f64],
124 ) -> Result<(f64, f64), OptimizeError> {
125 if mixed_1.len() != self.n_strategies_1 {
126 return Err(OptimizeError::ValueError(format!(
127 "mixed_1 length {} != n_strategies_1 {}",
128 mixed_1.len(),
129 self.n_strategies_1
130 )));
131 }
132 if mixed_2.len() != self.n_strategies_2 {
133 return Err(OptimizeError::ValueError(format!(
134 "mixed_2 length {} != n_strategies_2 {}",
135 mixed_2.len(),
136 self.n_strategies_2
137 )));
138 }
139
140 let tol = 1e-9;
141 let sum1: f64 = mixed_1.iter().sum();
142 if (sum1 - 1.0).abs() > tol {
143 return Err(OptimizeError::ValueError(format!(
144 "mixed_1 must sum to 1.0, got {}",
145 sum1
146 )));
147 }
148 let sum2: f64 = mixed_2.iter().sum();
149 if (sum2 - 1.0).abs() > tol {
150 return Err(OptimizeError::ValueError(format!(
151 "mixed_2 must sum to 1.0, got {}",
152 sum2
153 )));
154 }
155
156 let mut ep1 = 0.0_f64;
157 let mut ep2 = 0.0_f64;
158 for i in 0..self.n_strategies_1 {
159 for j in 0..self.n_strategies_2 {
160 let prob = mixed_1[i] * mixed_2[j];
161 ep1 += prob * self.payoff_matrix_1[[i, j]];
162 ep2 += prob * self.payoff_matrix_2[[i, j]];
163 }
164 }
165 Ok((ep1, ep2))
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct NashEquilibrium {
172 pub strategy_1: Vec<f64>,
174 pub strategy_2: Vec<f64>,
176 pub payoff_1: f64,
178 pub payoff_2: f64,
180 pub is_pure: bool,
182}
183
184pub fn find_pure_nash_equilibria(game: &NormalFormGame) -> Vec<NashEquilibrium> {
190 let n1 = game.n_strategies_1;
191 let n2 = game.n_strategies_2;
192 let mut results = Vec::new();
193
194 for i in 0..n1 {
195 for j in 0..n2 {
196 let p1_val = game.payoff_matrix_1[[i, j]];
198 let p1_br = (0..n1).all(|i2| game.payoff_matrix_1[[i2, j]] <= p1_val + 1e-12);
199
200 let p2_val = game.payoff_matrix_2[[i, j]];
202 let p2_br = (0..n2).all(|j2| game.payoff_matrix_2[[i, j2]] <= p2_val + 1e-12);
203
204 if p1_br && p2_br {
205 let mut s1 = vec![0.0; n1];
206 let mut s2 = vec![0.0; n2];
207 s1[i] = 1.0;
208 s2[j] = 1.0;
209 results.push(NashEquilibrium {
210 strategy_1: s1,
211 strategy_2: s2,
212 payoff_1: p1_val,
213 payoff_2: p2_val,
214 is_pure: true,
215 });
216 }
217 }
218 }
219 results
220}
221
222pub fn find_all_nash_equilibria(
235 game: &NormalFormGame,
236 tol: f64,
237) -> Result<Vec<NashEquilibrium>, OptimizeError> {
238 let n1 = game.n_strategies_1;
239 let n2 = game.n_strategies_2;
240
241 let mut results = find_pure_nash_equilibria(game);
243
244 for supp1_mask in 1u64..(1u64 << n1) {
248 let supp1: Vec<usize> = (0..n1).filter(|&i| (supp1_mask >> i) & 1 == 1).collect();
249
250 for supp2_mask in 1u64..(1u64 << n2) {
251 let supp2: Vec<usize> = (0..n2).filter(|&j| (supp2_mask >> j) & 1 == 1).collect();
252
253 let k1 = supp1.len();
254 let k2 = supp2.len();
255
256 if k1 == 1 && k2 == 1 {
258 continue;
259 }
260
261 if let Some(ne) = solve_support_ne(game, &supp1, &supp2, tol) {
263 let is_duplicate = results.iter().any(|existing| {
265 strategies_approx_equal(&existing.strategy_1, &ne.strategy_1, tol)
266 && strategies_approx_equal(&existing.strategy_2, &ne.strategy_2, tol)
267 });
268 if !is_duplicate {
269 results.push(ne);
270 }
271 }
272 }
273 }
274
275 Ok(results)
276}
277
278fn solve_support_ne(
283 game: &NormalFormGame,
284 supp1: &[usize],
285 supp2: &[usize],
286 tol: f64,
287) -> Option<NashEquilibrium> {
288 let k1 = supp1.len();
289 let k2 = supp2.len();
290
291 let q = solve_indifference(&game.payoff_matrix_1, supp1, supp2, tol)?;
297
298 let p = solve_indifference(&game.payoff_matrix_2.t().to_owned(), supp2, supp1, tol)?;
301
302 let v1: f64 = supp1
305 .iter()
306 .enumerate()
307 .map(|(idx, &i)| {
308 supp2
309 .iter()
310 .enumerate()
311 .map(|(jdx, &j)| game.payoff_matrix_1[[i, j]] * q[jdx])
312 .sum::<f64>()
313 * p[idx]
314 })
315 .sum();
316
317 for i in 0..game.n_strategies_1 {
318 if !supp1.contains(&i) {
319 let dev_payoff: f64 = supp2
320 .iter()
321 .enumerate()
322 .map(|(jdx, &j)| game.payoff_matrix_1[[i, j]] * q[jdx])
323 .sum();
324 if dev_payoff > v1 + tol {
325 return None;
326 }
327 }
328 }
329
330 let v2: f64 = supp2
331 .iter()
332 .enumerate()
333 .map(|(jdx, &j)| {
334 supp1
335 .iter()
336 .enumerate()
337 .map(|(idx, &i)| game.payoff_matrix_2[[i, j]] * p[idx])
338 .sum::<f64>()
339 * q[jdx]
340 })
341 .sum();
342
343 for j in 0..game.n_strategies_2 {
344 if !supp2.contains(&j) {
345 let dev_payoff: f64 = supp1
346 .iter()
347 .enumerate()
348 .map(|(idx, &i)| game.payoff_matrix_2[[i, j]] * p[idx])
349 .sum();
350 if dev_payoff > v2 + tol {
351 return None;
352 }
353 }
354 }
355
356 let n1 = game.n_strategies_1;
358 let n2 = game.n_strategies_2;
359 let mut strategy_1 = vec![0.0; n1];
360 let mut strategy_2 = vec![0.0; n2];
361 for (idx, &i) in supp1.iter().enumerate() {
362 strategy_1[i] = p[idx];
363 }
364 for (jdx, &j) in supp2.iter().enumerate() {
365 strategy_2[j] = q[jdx];
366 }
367
368 let is_pure = k1 == 1 && k2 == 1;
369
370 Some(NashEquilibrium {
371 strategy_1,
372 strategy_2,
373 payoff_1: v1,
374 payoff_2: v2,
375 is_pure,
376 })
377}
378
379fn solve_indifference(
388 payoff: &Array2<f64>,
389 supp_row: &[usize],
390 supp_col: &[usize],
391 tol: f64,
392) -> Option<Vec<f64>> {
393 let k_col = supp_col.len();
394 let k_row = supp_row.len();
395
396 if k_col == 1 {
397 return Some(vec![1.0]);
399 }
400
401 if k_row != k_col {
410 }
414
415 let n_eq = k_row; let n_var = k_col;
420
421 let mut mat = vec![0.0_f64; n_eq * n_var];
422 let mut rhs = vec![0.0_f64; n_eq];
423
424 for eq_idx in 0..(k_row - 1) {
426 let i0 = supp_row[0];
427 let i1 = supp_row[eq_idx + 1];
428 for (col_idx, &j) in supp_col.iter().enumerate() {
429 mat[eq_idx * n_var + col_idx] = payoff[[i0, j]] - payoff[[i1, j]];
430 }
431 rhs[eq_idx] = 0.0;
432 }
433
434 let simplex_row = k_row - 1;
436 for col_idx in 0..k_col {
437 mat[simplex_row * n_var + col_idx] = 1.0;
438 }
439 rhs[simplex_row] = 1.0;
440
441 let q = if n_eq == n_var {
443 gaussian_elimination(&mat, &rhs, n_var)?
444 } else {
445 least_squares_solve(&mat, &rhs, n_eq, n_var)?
447 };
448
449 if q.iter().any(|&v| v < -tol) {
451 return None;
452 }
453 let s: f64 = q.iter().sum();
454 if (s - 1.0).abs() > tol * 10.0 {
455 return None;
456 }
457
458 let q_clamped: Vec<f64> = q.iter().map(|&v| v.max(0.0)).collect();
460 let s2: f64 = q_clamped.iter().sum();
461 if s2 < tol {
462 return None;
463 }
464 let q_normalized: Vec<f64> = q_clamped.iter().map(|&v| v / s2).collect();
465
466 if q_normalized.iter().any(|&v| v < tol) {
468 return None;
469 }
470
471 Some(q_normalized)
472}
473
474fn gaussian_elimination(mat: &[f64], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
477 let mut a = mat.to_vec();
478 let mut b = rhs.to_vec();
479
480 for col in 0..n {
481 let pivot_row = (col..n).max_by(|&r1, &r2| {
483 a[r1 * n + col]
484 .abs()
485 .partial_cmp(&a[r2 * n + col].abs())
486 .unwrap_or(std::cmp::Ordering::Equal)
487 })?;
488
489 if a[pivot_row * n + col].abs() < 1e-14 {
490 return None;
491 }
492
493 if pivot_row != col {
495 for k in 0..n {
496 a.swap(col * n + k, pivot_row * n + k);
497 }
498 b.swap(col, pivot_row);
499 }
500
501 let pivot = a[col * n + col];
502 for row in (col + 1)..n {
503 let factor = a[row * n + col] / pivot;
504 for k in col..n {
505 let val = a[col * n + k] * factor;
506 a[row * n + k] -= val;
507 }
508 b[row] -= b[col] * factor;
509 }
510 }
511
512 let mut x = vec![0.0_f64; n];
514 for i in (0..n).rev() {
515 let mut sum = b[i];
516 for j in (i + 1)..n {
517 sum -= a[i * n + j] * x[j];
518 }
519 if a[i * n + i].abs() < 1e-14 {
520 return None;
521 }
522 x[i] = sum / a[i * n + i];
523 }
524 Some(x)
525}
526
527fn least_squares_solve(mat: &[f64], rhs: &[f64], n_eq: usize, n_var: usize) -> Option<Vec<f64>> {
529 let mut ata = vec![0.0_f64; n_var * n_var];
531 for i in 0..n_var {
532 for j in 0..n_var {
533 for k in 0..n_eq {
534 ata[i * n_var + j] += mat[k * n_var + i] * mat[k * n_var + j];
535 }
536 }
537 }
538 let mut atb = vec![0.0_f64; n_var];
540 for i in 0..n_var {
541 for k in 0..n_eq {
542 atb[i] += mat[k * n_var + i] * rhs[k];
543 }
544 }
545 gaussian_elimination(&ata, &atb, n_var)
546}
547
548fn strategies_approx_equal(a: &[f64], b: &[f64], tol: f64) -> bool {
550 if a.len() != b.len() {
551 return false;
552 }
553 a.iter()
554 .zip(b.iter())
555 .all(|(x, y)| (x - y).abs() < tol * 10.0)
556}
557
558pub fn best_response_1(game: &NormalFormGame, opponent_mixed: &[f64]) -> Vec<usize> {
562 let n1 = game.n_strategies_1;
563 let n2 = game.n_strategies_2;
564
565 if opponent_mixed.len() != n2 {
566 return Vec::new();
567 }
568
569 let payoffs: Vec<f64> = (0..n1)
570 .map(|i| {
571 (0..n2)
572 .map(|j| game.payoff_matrix_1[[i, j]] * opponent_mixed[j])
573 .sum::<f64>()
574 })
575 .collect();
576
577 let max_payoff = payoffs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
578 (0..n1)
579 .filter(|&i| (payoffs[i] - max_payoff).abs() < 1e-9)
580 .collect()
581}
582
583pub fn best_response_2(game: &NormalFormGame, opponent_mixed: &[f64]) -> Vec<usize> {
587 let n1 = game.n_strategies_1;
588 let n2 = game.n_strategies_2;
589
590 if opponent_mixed.len() != n1 {
591 return Vec::new();
592 }
593
594 let payoffs: Vec<f64> = (0..n2)
595 .map(|j| {
596 (0..n1)
597 .map(|i| game.payoff_matrix_2[[i, j]] * opponent_mixed[i])
598 .sum::<f64>()
599 })
600 .collect();
601
602 let max_payoff = payoffs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
603 (0..n2)
604 .filter(|&j| (payoffs[j] - max_payoff).abs() < 1e-9)
605 .collect()
606}
607
608pub fn iterated_elimination(game: &mut NormalFormGame) -> (Vec<usize>, Vec<usize>) {
617 let n1 = game.n_strategies_1;
618 let n2 = game.n_strategies_2;
619
620 let mut active_1: Vec<usize> = (0..n1).collect();
621 let mut active_2: Vec<usize> = (0..n2).collect();
622
623 let mut changed = true;
624 while changed {
625 changed = false;
626
627 let mut to_remove_1: Vec<usize> = Vec::new();
629 for &i in &active_1 {
630 if is_strictly_dominated_1(&game.payoff_matrix_1, i, &active_1, &active_2) {
631 to_remove_1.push(i);
632 changed = true;
633 }
634 }
635 active_1.retain(|i| !to_remove_1.contains(i));
636
637 let mut to_remove_2: Vec<usize> = Vec::new();
639 for &j in &active_2 {
640 if is_strictly_dominated_2(&game.payoff_matrix_2, j, &active_1, &active_2) {
641 to_remove_2.push(j);
642 changed = true;
643 }
644 }
645 active_2.retain(|j| !to_remove_2.contains(j));
646 }
647
648 (active_1, active_2)
649}
650
651fn is_strictly_dominated_1(
653 payoff: &Array2<f64>,
654 i: usize,
655 active_1: &[usize],
656 active_2: &[usize],
657) -> bool {
658 let k1 = active_1.len();
659 for &i2 in active_1 {
665 if i2 == i {
666 continue;
667 }
668 let dominates = active_2.iter().all(|&j| payoff[[i2, j]] > payoff[[i, j]]);
669 if dominates {
670 return true;
671 }
672 }
673 if k1 > 1 {
675 let alpha = 1.0 / (k1 - 1) as f64;
676 for &j in active_2 {
677 let mixed_val: f64 = active_1
678 .iter()
679 .filter(|&&i2| i2 != i)
680 .map(|&i2| payoff[[i2, j]] * alpha)
681 .sum();
682 if mixed_val <= payoff[[i, j]] {
683 return false;
684 }
685 }
686 if active_1.len() > 1 {
688 let all_dominated = active_2.iter().all(|&j| {
689 let mixed_val: f64 = active_1
690 .iter()
691 .filter(|&&i2| i2 != i)
692 .map(|&i2| payoff[[i2, j]] * alpha)
693 .sum();
694 mixed_val > payoff[[i, j]]
695 });
696 if all_dominated {
697 return true;
698 }
699 }
700 }
701 false
702}
703
704fn is_strictly_dominated_2(
706 payoff: &Array2<f64>,
707 j: usize,
708 active_1: &[usize],
709 active_2: &[usize],
710) -> bool {
711 let k2 = active_2.len();
712 for &j2 in active_2 {
713 if j2 == j {
714 continue;
715 }
716 let dominates = active_1.iter().all(|&i| payoff[[i, j2]] > payoff[[i, j]]);
717 if dominates {
718 return true;
719 }
720 }
721 if k2 > 1 {
722 let alpha = 1.0 / (k2 - 1) as f64;
723 let all_dominated = active_1.iter().all(|&i| {
724 let mixed_val: f64 = active_2
725 .iter()
726 .filter(|&&j2| j2 != j)
727 .map(|&j2| payoff[[i, j2]] * alpha)
728 .sum();
729 mixed_val > payoff[[i, j]]
730 });
731 if all_dominated {
732 return true;
733 }
734 }
735 false
736}
737
738pub fn dominant_strategy_equilibrium(game: &NormalFormGame) -> Option<(usize, usize)> {
746 let n1 = game.n_strategies_1;
747 let n2 = game.n_strategies_2;
748
749 let ds1 = (0..n1).find(|&i| {
751 (0..n1)
752 .filter(|&i2| i2 != i)
753 .all(|i2| (0..n2).all(|j| game.payoff_matrix_1[[i, j]] > game.payoff_matrix_1[[i2, j]]))
754 });
755
756 let ds2 = (0..n2).find(|&j| {
758 (0..n2)
759 .filter(|&j2| j2 != j)
760 .all(|j2| (0..n1).all(|i| game.payoff_matrix_2[[i, j]] > game.payoff_matrix_2[[i, j2]]))
761 });
762
763 match (ds1, ds2) {
764 (Some(i), Some(j)) => Some((i, j)),
765 _ => None,
766 }
767}
768
769pub fn replicator_dynamics(
783 payoff_matrix: ArrayView2<f64>,
784 initial_population: &[f64],
785 dt: f64,
786 n_steps: usize,
787) -> Vec<Vec<f64>> {
788 let n = payoff_matrix.nrows();
789 if n == 0 || initial_population.len() != n {
790 return Vec::new();
791 }
792
793 let mut x = initial_population.to_vec();
794 let s: f64 = x.iter().sum();
796 if s > 0.0 {
797 x.iter_mut().for_each(|v| *v /= s);
798 }
799
800 let mut trajectory = vec![x.clone()];
801
802 for _ in 0..n_steps {
803 let ax: Vec<f64> = (0..n)
805 .map(|i| (0..n).map(|j| payoff_matrix[[i, j]] * x[j]).sum::<f64>())
806 .collect();
807
808 let mean_fitness: f64 = x.iter().zip(ax.iter()).map(|(xi, axi)| xi * axi).sum();
810
811 let mut x_new: Vec<f64> = x
813 .iter()
814 .zip(ax.iter())
815 .map(|(&xi, &axi)| xi + dt * xi * (axi - mean_fitness))
816 .collect();
817
818 x_new.iter_mut().for_each(|v| {
820 if *v < 0.0 {
821 *v = 0.0;
822 }
823 });
824 let total: f64 = x_new.iter().sum();
825 if total > 1e-15 {
826 x_new.iter_mut().for_each(|v| *v /= total);
827 }
828
829 x = x_new.clone();
830 trajectory.push(x_new);
831 }
832
833 trajectory
834}
835
836pub fn find_ess(payoff_matrix: ArrayView2<f64>) -> Vec<Vec<f64>> {
848 let n = payoff_matrix.nrows();
849 if n == 0 {
850 return Vec::new();
851 }
852
853 let mut ess_list = Vec::new();
854
855 for i in 0..n {
857 let mut p = vec![0.0; n];
858 p[i] = 1.0;
859 if is_ess_pure(payoff_matrix, i, n) {
860 ess_list.push(p);
861 }
862 }
863
864 let fixed_points = find_interior_ess_candidates(payoff_matrix, n);
867 for fp in fixed_points {
868 if !ess_list
869 .iter()
870 .any(|e: &Vec<f64>| e.iter().zip(fp.iter()).all(|(a, b)| (a - b).abs() < 1e-6))
871 {
872 ess_list.push(fp);
873 }
874 }
875
876 ess_list
877}
878
879fn is_ess_pure(payoff: ArrayView2<f64>, i: usize, n: usize) -> bool {
881 let u_ii = payoff[[i, i]];
882 for j in 0..n {
883 if j == i {
884 continue;
885 }
886 let u_ji = payoff[[j, i]];
887 if u_ji > u_ii + 1e-12 {
888 return false;
890 }
891 if (u_ji - u_ii).abs() < 1e-12 {
892 let u_ij = payoff[[i, j]];
894 let u_jj = payoff[[j, j]];
895 if u_jj >= u_ij + 1e-12 {
896 return false;
897 }
898 }
899 }
900 true
901}
902
903fn find_interior_ess_candidates(payoff: ArrayView2<f64>, n: usize) -> Vec<Vec<f64>> {
906 if n > 4 {
907 return Vec::new(); }
909
910 let mut candidates: Vec<Vec<f64>> = Vec::new();
911 let tol = 1e-6;
912
913 let grid_points = if n == 2 {
915 (1..10)
916 .map(|k| {
917 let p = k as f64 / 10.0;
918 vec![p, 1.0 - p]
919 })
920 .collect::<Vec<_>>()
921 } else if n == 3 {
922 let mut pts = Vec::new();
923 for a in 1..9 {
924 for b in 1..(10 - a) {
925 let c = 10 - a - b;
926 if c > 0 {
927 pts.push(vec![a as f64 / 10.0, b as f64 / 10.0, c as f64 / 10.0]);
928 }
929 }
930 }
931 pts
932 } else {
933 vec![
935 vec![0.25, 0.25, 0.25, 0.25],
936 vec![0.4, 0.2, 0.2, 0.2],
937 vec![0.1, 0.5, 0.3, 0.1],
938 ]
939 };
940
941 for start in grid_points {
942 let traj = replicator_dynamics(payoff, &start, 0.01, 10000);
943 if let Some(final_state) = traj.last() {
944 let is_interior = final_state.iter().all(|&v| v > tol);
946 if is_interior {
947 let ax: Vec<f64> = (0..n)
949 .map(|i| (0..n).map(|j| payoff[[i, j]] * final_state[j]).sum::<f64>())
950 .collect();
951 let mean_f: f64 = final_state
952 .iter()
953 .zip(ax.iter())
954 .map(|(xi, axi)| xi * axi)
955 .sum();
956 let is_fp = final_state
957 .iter()
958 .zip(ax.iter())
959 .all(|(&xi, &axi)| (xi * (axi - mean_f)).abs() < 1e-5);
960
961 if is_fp {
962 let is_dup = candidates.iter().any(|c: &Vec<f64>| {
964 c.iter()
965 .zip(final_state.iter())
966 .all(|(a, b)| (a - b).abs() < 1e-5)
967 });
968 if !is_dup {
969 if verify_ess_stability(payoff, final_state, n, tol) {
971 candidates.push(final_state.clone());
972 }
973 }
974 }
975 }
976 }
977 }
978
979 candidates
980}
981
982fn verify_ess_stability(payoff: ArrayView2<f64>, p_star: &[f64], n: usize, tol: f64) -> bool {
984 let u_pp: f64 = (0..n)
986 .map(|i| {
987 (0..n)
988 .map(|j| p_star[i] * payoff[[i, j]] * p_star[j])
989 .sum::<f64>()
990 })
991 .sum();
992
993 for i in 0..n {
995 if p_star[i] < tol {
996 continue;
997 }
998 for j in 0..n {
999 if j == i || p_star[j] < tol {
1000 continue;
1001 }
1002 let eps = 0.01;
1004 let q: Vec<f64> = p_star
1005 .iter()
1006 .enumerate()
1007 .map(|(k, &pk)| {
1008 if k == j {
1009 pk + eps * (1.0 - pk)
1010 } else {
1011 pk * (1.0 - eps)
1012 }
1013 })
1014 .collect();
1015 let s: f64 = q.iter().sum();
1017 let q: Vec<f64> = q.iter().map(|v| v / s).collect();
1018
1019 let u_qq: f64 = (0..n)
1021 .map(|a| (0..n).map(|b| q[a] * payoff[[a, b]] * q[b]).sum::<f64>())
1022 .sum();
1023 let u_pq: f64 = (0..n)
1024 .map(|a| {
1025 (0..n)
1026 .map(|b| p_star[a] * payoff[[a, b]] * q[b])
1027 .sum::<f64>()
1028 })
1029 .sum();
1030
1031 let u_qp: f64 = (0..n)
1033 .map(|a| {
1034 (0..n)
1035 .map(|b| q[a] * payoff[[a, b]] * p_star[b])
1036 .sum::<f64>()
1037 })
1038 .sum();
1039
1040 if u_qp > u_pp + tol {
1041 return false;
1042 }
1043 if (u_qp - u_pp).abs() < tol && u_qq >= u_pq - tol {
1044 return false;
1045 }
1046 }
1047 }
1048 true
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054 use approx::assert_relative_eq;
1055 use scirs2_core::ndarray::array;
1056
1057 #[test]
1058 fn test_normal_form_game_construction() {
1059 let p1 = array![[3.0, 0.0], [5.0, 1.0]];
1060 let p2 = array![[3.0, 5.0], [0.0, 1.0]];
1061 let game = NormalFormGame::new(p1, p2).expect("valid game");
1062 assert_eq!(game.n_strategies_1, 2);
1063 assert_eq!(game.n_strategies_2, 2);
1064 }
1065
1066 #[test]
1067 fn test_normal_form_shape_mismatch() {
1068 let p1 = array![[1.0, 2.0]];
1069 let p2 = array![[1.0], [2.0]];
1070 assert!(NormalFormGame::new(p1, p2).is_err());
1071 }
1072
1073 #[test]
1074 fn test_zero_sum_constructor() {
1075 let payoff = array![[1.0, -1.0], [-1.0, 1.0]];
1076 let game = NormalFormGame::zero_sum(payoff);
1077 assert_eq!(game.payoff_matrix_2[[0, 0]], -1.0);
1079 assert_eq!(game.payoff_matrix_2[[0, 1]], 1.0);
1080 }
1081
1082 #[test]
1083 fn test_symmetric_game() {
1084 let payoff = array![[0.0, -1.0, 1.0], [1.0, 0.0, -1.0], [-1.0, 1.0, 0.0]];
1085 let game = NormalFormGame::symmetric(payoff).expect("valid symmetric game");
1086 assert_eq!(game.payoff_matrix_2[[1, 0]], game.payoff_matrix_1[[0, 1]]);
1088 }
1089
1090 #[test]
1091 fn test_payoff_lookup() {
1092 let p1 = array![[1.0, 2.0], [3.0, 4.0]];
1093 let p2 = array![[5.0, 6.0], [7.0, 8.0]];
1094 let game = NormalFormGame::new(p1, p2).expect("valid");
1095 let (a, b) = game.payoff(1, 0);
1096 assert_relative_eq!(a, 3.0);
1097 assert_relative_eq!(b, 7.0);
1098 }
1099
1100 #[test]
1101 fn test_expected_payoff() {
1102 let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
1104 let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
1105 let game = NormalFormGame::new(p1, p2).expect("valid");
1106 let (ep1, ep2) = game
1107 .expected_payoff(&[0.5, 0.5], &[0.5, 0.5])
1108 .expect("valid mixed strategies");
1109 assert_relative_eq!(ep1, 0.0, epsilon = 1e-10);
1110 assert_relative_eq!(ep2, 0.0, epsilon = 1e-10);
1111 }
1112
1113 #[test]
1114 fn test_expected_payoff_invalid_length() {
1115 let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
1116 let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
1117 let game = NormalFormGame::new(p1, p2).expect("valid");
1118 assert!(game.expected_payoff(&[1.0], &[0.5, 0.5]).is_err());
1119 }
1120
1121 #[test]
1122 fn test_prisoners_dilemma_pure_nash() {
1123 let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
1126 let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
1127 let game = NormalFormGame::new(p1, p2).expect("valid");
1128 let pure_ne = find_pure_nash_equilibria(&game);
1129 assert_eq!(pure_ne.len(), 1);
1130 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);
1133 }
1134
1135 #[test]
1136 fn test_coordination_game_multiple_pure_nash() {
1137 let p1 = array![[2.0, 0.0], [0.0, 1.0]];
1139 let p2 = array![[2.0, 0.0], [0.0, 1.0]];
1140 let game = NormalFormGame::new(p1, p2).expect("valid");
1141 let pure_ne = find_pure_nash_equilibria(&game);
1142 assert_eq!(pure_ne.len(), 2);
1143 }
1144
1145 #[test]
1146 fn test_matching_pennies_mixed_nash() {
1147 let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
1149 let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
1150 let game = NormalFormGame::new(p1, p2).expect("valid");
1151 let pure_ne = find_pure_nash_equilibria(&game);
1152 assert_eq!(pure_ne.len(), 0);
1153
1154 let all_ne = find_all_nash_equilibria(&game, 1e-9).expect("success");
1155 assert_eq!(all_ne.len(), 1);
1157 assert_relative_eq!(all_ne[0].strategy_1[0], 0.5, epsilon = 1e-6);
1158 assert_relative_eq!(all_ne[0].strategy_2[0], 0.5, epsilon = 1e-6);
1159 }
1160
1161 #[test]
1162 fn test_best_response_1() {
1163 let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
1165 let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
1166 let game = NormalFormGame::new(p1, p2).expect("valid");
1167 let br = best_response_1(&game, &[0.0, 1.0]);
1168 assert!(br.contains(&1)); }
1170
1171 #[test]
1172 fn test_best_response_2() {
1173 let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
1174 let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
1175 let game = NormalFormGame::new(p1, p2).expect("valid");
1176 let br = best_response_2(&game, &[0.0, 1.0]);
1177 assert!(br.contains(&1)); }
1179
1180 #[test]
1181 fn test_dominant_strategy_equilibrium() {
1182 let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
1184 let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
1185 let game = NormalFormGame::new(p1, p2).expect("valid");
1186 let ds = dominant_strategy_equilibrium(&game);
1187 assert_eq!(ds, Some((1, 1)));
1188 }
1189
1190 #[test]
1191 fn test_dominant_strategy_equilibrium_none() {
1192 let p1 = array![[1.0, -1.0], [-1.0, 1.0]];
1194 let p2 = array![[-1.0, 1.0], [1.0, -1.0]];
1195 let game = NormalFormGame::new(p1, p2).expect("valid");
1196 let ds = dominant_strategy_equilibrium(&game);
1197 assert!(ds.is_none());
1198 }
1199
1200 #[test]
1201 fn test_iterated_elimination() {
1202 let p1 = array![[-1.0, -3.0], [0.0, -2.0]];
1204 let p2 = array![[-1.0, 0.0], [-3.0, -2.0]];
1205 let mut game = NormalFormGame::new(p1, p2).expect("valid");
1206 let (s1, s2) = iterated_elimination(&mut game);
1207 assert_eq!(s1, vec![1]);
1208 assert_eq!(s2, vec![1]);
1209 }
1210
1211 #[test]
1212 fn test_replicator_dynamics_convergence() {
1213 let payoff = array![[0.0, 3.0], [1.0, 2.0]];
1216 let initial = vec![0.5, 0.5];
1217 let traj = replicator_dynamics(payoff.view(), &initial, 0.01, 1000);
1218 assert!(!traj.is_empty());
1219 let final_state = traj.last().expect("non-empty trajectory");
1221 let total: f64 = final_state.iter().sum();
1222 assert_relative_eq!(total, 1.0, epsilon = 1e-6);
1223 }
1224
1225 #[test]
1226 fn test_find_ess_pure() {
1227 let payoff = array![[0.0, -1.0, 1.0], [1.0, 0.0, -1.0], [-1.0, 1.0, 0.0]];
1229 let ess = find_ess(payoff.view());
1230 let pure_ess: Vec<_> = ess
1232 .iter()
1233 .filter(|e| e.iter().filter(|&&v| v > 0.01).count() == 1)
1234 .collect();
1235 assert_eq!(pure_ess.len(), 0);
1236 }
1237
1238 #[test]
1239 fn test_symmetric_game_invalid_shape() {
1240 let payoff = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1241 assert!(NormalFormGame::symmetric(payoff).is_err());
1242 }
1243}