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