diff --git a/Shayan1/Prod/nash/solver.py b/Shayan1/Prod/nash/solver.py new file mode 100644 index 0000000..ce94e21 --- /dev/null +++ b/Shayan1/Prod/nash/solver.py @@ -0,0 +1,223 @@ +import itertools + +import numpy as np + + +def find_pure_nash_equilibria(game): + """Brute-force search over all strategy profiles. Works for any number of players.""" + equilibria = [] + for profile in game.all_profiles(): + if _is_pure_nash(game, profile): + equilibria.append(profile) + return equilibria + + +def find_dominant_strategies(game): + """Find a strictly dominant strategy for each player, if one exists. + + A strategy *s* for player *i* is **strictly dominant** when it yields a + strictly higher payoff than every other strategy of player *i* against + *all* possible opponent profiles. + + Returns ``dict[int, int | None]`` mapping each player index to the index + of their strictly dominant strategy (or ``None`` when no such strategy + exists). Works for any number of players. + """ + counts = game.strategy_counts() + result = {} + for i in range(game.num_players): + if counts[i] < 2: + result[i] = None + continue + dominant = None + for s in range(counts[i]): + is_dominant = True + for other in range(counts[i]): + if other == s: + continue + # Compare s vs other across every opponent profile. + opp_ranges = [range(n) for j, n in enumerate(counts) if j != i] + for opp_profile in itertools.product(*opp_ranges): + profile_s = opp_profile[:i] + (s,) + opp_profile[i:] + profile_other = opp_profile[:i] + (other,) + opp_profile[i:] + if game.payoff(i, profile_s) <= game.payoff(i, profile_other): + is_dominant = False + break + if not is_dominant: + break + if is_dominant: + dominant = s + break + result[i] = dominant + return result + + +def _is_pure_nash(game, profile): + strategy_counts = game.strategy_counts() + for i in range(game.num_players): + current_payoff = game.payoff(i, profile) + for s in range(strategy_counts[i]): + if s == profile[i]: + continue + deviation = list(profile) + deviation[i] = s + if game.payoff(i, tuple(deviation)) > current_payoff: + return False + return True + + +def find_mixed_nash_equilibria(game, tol=1e-7): + """Support enumeration for 2-player games (assumes non-degenerate games, i.e. equal-size + supports). Returns list of (x, y) probability vectors. + """ + if game.num_players != 2: + raise NotImplementedError( + "Mixed Nash equilibrium solving is only implemented for 2-player games" + ) + + A, B = game.payoffs + m, n = A.shape + equilibria = [] + + for k in range(1, min(m, n) + 1): + for support_i in itertools.combinations(range(m), k): + for support_j in itertools.combinations(range(n), k): + y, v1 = _solve_indifference(A, support_i, support_j, tol) + x, v2 = _solve_indifference(B.T, support_j, support_i, tol) + if y is None or x is None: + continue + if _is_feasible(A, B, support_i, support_j, x, y, v1, v2, tol): + if not _already_found(equilibria, x, y, tol): + equilibria.append((x, y)) + + return equilibria + + +def _solve_support(A, B, support_i, support_j, tol): + y_result = _solve_indifference(A, support_i, support_j, tol) + x_result = _solve_indifference(B.T, support_j, support_i, tol) + if y_result is None or x_result is None: + return None, None + y, v1 = y_result + x, v2 = x_result + return x, y + + +def _solve_indifference(matrix, eq_indices, var_indices, tol): + """Solve for a probability distribution over var_indices that makes the player whose + payoff matrix is ``matrix`` indifferent among the strategies in ``eq_indices``. + """ + k = len(eq_indices) + # Build the linear system: for each eq_index except the last, the difference + # in expected payoffs between that index and the last must be zero. + if k == 1: + probs = np.zeros(matrix.shape[1]) + probs[list(var_indices)] = 1.0 + value = matrix[eq_indices[0], var_indices[0]] + if value < -tol: + return None, None + return np.clip(probs, 0, None), value + + A_mat = np.zeros((k, len(var_indices))) + b_vec = np.zeros(k) + last_row = matrix[eq_indices[-1], list(var_indices)] + for row_i in range(k - 1): + this_row = matrix[eq_indices[row_i], list(var_indices)] + A_mat[row_i] = this_row - last_row + b_vec[row_i] = 0.0 + A_mat[-1] = 1.0 + b_vec[-1] = 1.0 + + try: + probs_support = np.linalg.solve(A_mat, b_vec) + except np.linalg.LinAlgError: + return None, None + + # Check for negative probabilities in the solved support + if any(p < -tol for p in probs_support): + return None, None + probs = np.zeros(matrix.shape[1]) + for idx, v in enumerate(var_indices): + probs[v] = probs_support[idx] + probs = np.clip(probs, 0, None) + if np.sum(probs[list(var_indices)]) < tol: + return None, None + probs[list(var_indices)] /= np.sum(probs[list(var_indices)]) + value = matrix[eq_indices[0]] @ probs + return np.clip(probs, 0, None), value + + +def _is_feasible(A, B, support_i, support_j, x, y, v1, v2, tol): + m, n = A.shape + # All probabilities non-negative + if any(x[i] < -tol for i in support_i) or any(y[j] < -tol for j in support_j): + return False + # Best-response check: no strategy outside support gives higher payoff + for i in range(m): + if i not in support_i and A[i] @ y > v1 + tol: + return False + for j in range(n): + if j not in support_j and B[:, j] @ x > v2 + tol: + return False + return True + + +def _already_found(equilibria, x, y, tol): + for ex, ey in equilibria: + if np.allclose(x, ex, atol=tol) and np.allclose(y, ey, atol=tol): + return True + return False + + +def reduce_game(game): + """Iteratively eliminate strictly dominated strategies for each player. + + Returns a tuple of (reduced_game, logs). + """ + current_game = game + logs = [] + while True: + eliminated_any = False + counts = current_game.strategy_counts() + for i in range(current_game.num_players): + if counts[i] < 2: + continue + dominated_idx = None + for s in range(counts[i]): + for other in range(counts[i]): + if other == s: + continue + is_dominated = True + opp_ranges = [range(n) for j, n in enumerate(counts) if j != i] + for opp_profile in itertools.product(*opp_ranges): + profile_s = opp_profile[:i] + (s,) + opp_profile[i:] + profile_other = opp_profile[:i] + (other,) + opp_profile[i:] + if current_game.payoff(i, profile_other) <= current_game.payoff(i, profile_s): + is_dominated = False + break + if is_dominated: + dominated_idx = s + break + if dominated_idx is not None: + break + + if dominated_idx is not None: + player_name = current_game.players[i] + strategy_name = current_game.strategies[i][dominated_idx] + logs.append(f"Player '{player_name}' eliminated dominated strategy '{strategy_name}'") + new_players = current_game.players + new_strategies = [] + for j, strats in enumerate(current_game.strategies): + if j == i: + new_strategies.append(strats[:dominated_idx] + strats[dominated_idx+1:]) + else: + new_strategies.append(list(strats)) + new_payoffs = [] + for payoff in current_game.payoffs: + new_payoffs.append(np.delete(payoff, dominated_idx, axis=i)) + current_game = current_game.__class__(new_players, new_strategies, new_payoffs) + eliminated_any = True + break + if not eliminated_any: + break + return current_game, logs