Import source: Shayan1/Prod/nash/solver

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:14:43 +03:30
parent 9e3de94567
commit fb8525aa60
+223
View File
@@ -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