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]