Spaces:
Paused
Paused
File size: 5,634 Bytes
f7e2ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """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]
|