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:
@@ -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
|
||||
Reference in New Issue
Block a user