jtowarek's picture
Upload folder using huggingface_hub
f7e2ae6 verified
"""Dynamic game creation API for building games at runtime."""
from __future__ import annotations
from typing import Callable
from common.games import GameConfig, GAMES, _matrix_payoff_fn
from constant_definitions.nplayer.dynamic_constants import (
MIN_ACTIONS,
MAX_ACTIONS,
DYNAMIC_DEFAULT_ROUNDS,
REGISTRY_PREFIX,
)
_ONE = int(bool(True))
_TWO = _ONE + _ONE
def _validate_actions(actions: list[str]) -> None:
"""Raise ValueError if action list is invalid."""
if len(actions) < MIN_ACTIONS:
raise ValueError(
f"Need at least {MIN_ACTIONS} actions, got {len(actions)}"
)
if len(actions) > MAX_ACTIONS:
raise ValueError(
f"At most {MAX_ACTIONS} actions allowed, got {len(actions)}"
)
if len(actions) != len(set(actions)):
raise ValueError("Duplicate actions are not allowed")
def _validate_matrix(
actions: list[str],
payoff_matrix: dict[tuple[str, str], tuple[float, float]],
) -> None:
"""Raise ValueError if the matrix is incomplete or has invalid keys."""
expected = {(a, b) for a in actions for b in actions}
actual = set(payoff_matrix.keys())
missing = expected - actual
if missing:
raise ValueError(f"Payoff matrix is missing entries: {missing}")
extra = actual - expected
if extra:
raise ValueError(f"Payoff matrix has unknown action pairs: {extra}")
def create_matrix_game(
name: str,
actions: list[str],
payoff_matrix: dict[tuple[str, str], tuple[float, float]],
*,
description: str = "",
default_rounds: int = DYNAMIC_DEFAULT_ROUNDS,
register: bool = False,
) -> GameConfig:
"""Create a GameConfig backed by an explicit payoff matrix.
Parameters
----------
name:
Display name for the game.
actions:
List of action strings available to both players.
payoff_matrix:
``{(player_action, opponent_action): (player_pay, opponent_pay)}``.
description:
Human-readable description of the game rules.
default_rounds:
Number of rounds when the caller does not specify.
register:
If ``True``, add the game to the global ``GAMES`` registry using the
key ``dynamic_<name>``.
Returns
-------
GameConfig
"""
_validate_actions(actions)
_validate_matrix(actions, payoff_matrix)
config = GameConfig(
name=name,
description=description or f"Dynamic matrix game: {name}",
actions=list(actions),
game_type="matrix",
default_rounds=default_rounds,
payoff_fn=_matrix_payoff_fn(dict(payoff_matrix)),
)
if register:
key = REGISTRY_PREFIX + name
GAMES[key] = config
return config
def create_symmetric_game(
name: str,
actions: list[str],
payoffs: dict[tuple[str, str], float],
*,
description: str = "",
default_rounds: int = DYNAMIC_DEFAULT_ROUNDS,
register: bool = False,
) -> GameConfig:
"""Create a symmetric GameConfig from single-value payoffs.
In a symmetric game, ``payoff(A, B)`` for the row player equals
``payoff(B, A)`` for the column player. You only specify the row-player
payoff for each cell and the full matrix is derived.
Parameters
----------
name:
Display name.
actions:
List of action strings.
payoffs:
``{(my_action, their_action): my_payoff}``.
description:
Human-readable description.
default_rounds:
Number of rounds.
register:
If ``True``, register as ``dynamic_<name>``.
Returns
-------
GameConfig
"""
_validate_actions(actions)
expected = {(a, b) for a in actions for b in actions}
actual = set(payoffs.keys())
missing = expected - actual
if missing:
raise ValueError(f"Symmetric payoff table is missing entries: {missing}")
full_matrix: dict[tuple[str, str], tuple[float, float]] = {}
for a in actions:
for b in actions:
full_matrix[(a, b)] = (payoffs[(a, b)], payoffs[(b, a)])
return create_matrix_game(
name,
actions,
full_matrix,
description=description,
default_rounds=default_rounds,
register=register,
)
def create_custom_game(
name: str,
actions: list[str],
payoff_fn: Callable[[str, str], tuple[float, float]],
*,
game_type: str = "matrix",
description: str = "",
default_rounds: int = DYNAMIC_DEFAULT_ROUNDS,
register: bool = False,
) -> GameConfig:
"""Create a GameConfig with an arbitrary payoff function.
Parameters
----------
name:
Display name.
actions:
List of action strings.
payoff_fn:
``(player_action, opponent_action) -> (player_pay, opponent_pay)``.
game_type:
Game type tag (default ``"matrix"``).
description:
Human-readable description.
default_rounds:
Number of rounds.
register:
If ``True``, register as ``dynamic_<name>``.
Returns
-------
GameConfig
"""
_validate_actions(actions)
config = GameConfig(
name=name,
description=description or f"Dynamic custom game: {name}",
actions=list(actions),
game_type=game_type,
default_rounds=default_rounds,
payoff_fn=payoff_fn,
)
if register:
key = REGISTRY_PREFIX + name
GAMES[key] = config
return config
def unregister_game(key: str) -> None:
"""Remove a game from the global ``GAMES`` registry.
Raises ``KeyError`` if the key is not found.
"""
del GAMES[key]