Import source: Shayan1/Prod/nash/game
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,89 @@
|
|||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class GameValidationError(ValueError):
|
||||||
|
"""Base exception for all game validation errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyPlayersError(GameValidationError):
|
||||||
|
"""Raised when the players list is empty."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyStrategiesError(GameValidationError):
|
||||||
|
"""Raised when a player's strategy list is empty."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DimensionMismatchError(GameValidationError):
|
||||||
|
"""Raised when players, strategies, and payoffs lengths do not match."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PayoffShapeError(GameValidationError):
|
||||||
|
"""Raised when a payoff tensor has an incorrect shape."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
"""A normal-form game: players, their strategies, and a payoff tensor per player."""
|
||||||
|
|
||||||
|
def __init__(self, players, strategies, payoffs):
|
||||||
|
if not players:
|
||||||
|
raise EmptyPlayersError("players list must not be empty")
|
||||||
|
if len(players) != len(strategies) or len(players) != len(payoffs):
|
||||||
|
raise DimensionMismatchError(
|
||||||
|
f"players ({len(players)}), strategies ({len(strategies)}), "
|
||||||
|
f"and payoffs ({len(payoffs)}) must have the same length"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.players = list(players)
|
||||||
|
self.strategies = [list(s) for s in strategies]
|
||||||
|
self.num_players = len(players)
|
||||||
|
|
||||||
|
for i, strats in enumerate(self.strategies):
|
||||||
|
if not strats:
|
||||||
|
raise EmptyStrategiesError(f"strategies[{i}] ({players[i]!r}) must not be empty")
|
||||||
|
|
||||||
|
expected_shape = tuple(len(s) for s in self.strategies)
|
||||||
|
self.payoffs = []
|
||||||
|
for i, p in enumerate(payoffs):
|
||||||
|
arr = np.array(p, dtype=float)
|
||||||
|
if arr.shape != expected_shape:
|
||||||
|
raise PayoffShapeError(
|
||||||
|
f"payoffs[{i}] ({players[i]!r}) has shape {arr.shape}, "
|
||||||
|
f"expected {expected_shape} "
|
||||||
|
f"(one dimension per player, sized to that player's strategy count)"
|
||||||
|
)
|
||||||
|
self.payoffs.append(arr)
|
||||||
|
@classmethod
|
||||||
|
def load_from_json(cls, path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
raise ValueError(f"Failed to load game from {path!r}: {e}") from e
|
||||||
|
try:
|
||||||
|
return cls(data["players"], data["strategies"], data["payoffs"])
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Game JSON is missing required field: {e}. "
|
||||||
|
f"Expected keys: 'players', 'strategies', 'payoffs'"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def strategy_counts(self):
|
||||||
|
return [len(s) for s in self.strategies]
|
||||||
|
|
||||||
|
def payoff(self, player_index, profile):
|
||||||
|
"""profile is a tuple of strategy indices, one per player."""
|
||||||
|
return self.payoffs[player_index][profile]
|
||||||
|
|
||||||
|
def all_profiles(self):
|
||||||
|
return itertools.product(*(range(n) for n in self.strategy_counts()))
|
||||||
|
|
||||||
|
def strategy_name(self, player_index, strategy_index):
|
||||||
|
return self.strategies[player_index][strategy_index]
|
||||||
Reference in New Issue
Block a user