File size: 10,541 Bytes
f7e2ae6
 
 
 
 
 
 
 
 
 
688c130
 
 
f7e2ae6
688c130
 
 
 
 
f7e2ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
688c130
f7e2ae6
688c130
 
 
 
 
 
 
 
 
 
f7e2ae6
 
 
 
 
 
688c130
f7e2ae6
 
 
 
 
 
688c130
f7e2ae6
 
 
 
 
 
688c130
f7e2ae6
 
 
 
 
 
 
688c130
f7e2ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed4bdac
 
f7e2ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed4bdac
 
 
 
f7e2ae6
ed4bdac
f7e2ae6
 
 
 
 
ed4bdac
f7e2ae6
ed4bdac
 
f7e2ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688c130
ed4bdac
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""Game configuration registry and payoff computation for KantBench."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable

from constant_definitions.game_constants import (
    DEFAULT_ZERO_FLOAT,
    DEFAULT_ZERO_INT,
    PD_CC_PAYOFF, PD_CD_PAYOFF, PD_DC_PAYOFF, PD_DD_PAYOFF,
    SH_SS_PAYOFF, SH_SH_PAYOFF, SH_HS_PAYOFF, SH_HH_PAYOFF,
    HD_HH_PAYOFF, HD_HD_PAYOFF, HD_DH_PAYOFF, HD_DD_PAYOFF,
    ULTIMATUM_POT,
    TRUST_MULTIPLIER, TRUST_ENDOWMENT,
    PG_MULTIPLIER_NUMERATOR, PG_MULTIPLIER_DENOMINATOR,
    PG_ENDOWMENT, PG_DEFAULT_NUM_PLAYERS,
    DEFAULT_NUM_ROUNDS, SINGLE_SHOT_ROUNDS, DEFAULT_TWO_PLAYERS,
    OPPONENT_MODE_STRATEGY,
)

# ---------------------------------------------------------------------------
# GameConfig dataclass
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class GameConfig:
    """Immutable specification for a single game type."""

    name: str
    description: str
    actions: list[str]
    game_type: str
    default_rounds: int
    payoff_fn: Callable
    num_players: int = DEFAULT_TWO_PLAYERS
    applied_variants: tuple[str, ...] = ()
    base_game_key: str = ""
    enforcement: str = ""
    penalty_numerator: int = DEFAULT_ZERO_INT
    penalty_denominator: int = SINGLE_SHOT_ROUNDS
    allow_side_payments: bool = False
    opponent_mode: str = OPPONENT_MODE_STRATEGY
    opponent_actions: tuple[str, ...] | None = None


# ---------------------------------------------------------------------------
# Matrix-game payoff helpers
# ---------------------------------------------------------------------------

_PD_MATRIX = {
    ("cooperate", "cooperate"): (float(PD_CC_PAYOFF), float(PD_CC_PAYOFF)),
    ("cooperate", "defect"):    (float(PD_CD_PAYOFF), float(PD_DC_PAYOFF)),
    ("defect", "cooperate"):    (float(PD_DC_PAYOFF), float(PD_CD_PAYOFF)),
    ("defect", "defect"):       (float(PD_DD_PAYOFF), float(PD_DD_PAYOFF)),
}

_SH_MATRIX = {
    ("stag", "stag"): (float(SH_SS_PAYOFF), float(SH_SS_PAYOFF)),
    ("stag", "hare"): (float(SH_SH_PAYOFF), float(SH_HS_PAYOFF)),
    ("hare", "stag"): (float(SH_HS_PAYOFF), float(SH_SH_PAYOFF)),
    ("hare", "hare"): (float(SH_HH_PAYOFF), float(SH_HH_PAYOFF)),
}

_HD_MATRIX = {
    ("hawk", "hawk"): (float(HD_HH_PAYOFF), float(HD_HH_PAYOFF)),
    ("hawk", "dove"): (float(HD_HD_PAYOFF), float(HD_DH_PAYOFF)),
    ("dove", "hawk"): (float(HD_DH_PAYOFF), float(HD_HD_PAYOFF)),
    ("dove", "dove"): (float(HD_DD_PAYOFF), float(HD_DD_PAYOFF)),
}


def _matrix_payoff_fn(matrix: dict) -> Callable:
    """Return a payoff function backed by a pre-built matrix dict."""

    def _payoff(player_action: str, opponent_action: str) -> tuple[float, float]:
        return matrix[(player_action, opponent_action)]

    return _payoff


# ---------------------------------------------------------------------------
# Computed payoff functions
# ---------------------------------------------------------------------------


def _parse_action_amount(action: str) -> int:
    """Extract the integer suffix from an action string like 'offer_5'."""
    parts = action.rsplit("_", maxsplit=SINGLE_SHOT_ROUNDS)
    return int(parts[SINGLE_SHOT_ROUNDS])


def _ultimatum_payoff(player_action: str, opponent_action: str) -> tuple[float, float]:
    """Compute Ultimatum Game payoffs.

    The player chooses an offer amount; the opponent accepts or rejects.
    """
    offer = _parse_action_amount(player_action)

    if opponent_action == "reject":
        return (DEFAULT_ZERO_FLOAT, DEFAULT_ZERO_FLOAT)

    # accepted
    player_payoff = float(ULTIMATUM_POT - offer)
    opponent_payoff = float(offer)
    return (player_payoff, opponent_payoff)


def _trust_payoff(player_action: str, opponent_action: str) -> tuple[float, float]:
    """Compute Trust Game payoffs.

    The player invests X from their endowment. The opponent receives
    X * multiplier and returns Y of that amount.
    """
    investment = _parse_action_amount(player_action)
    returned = _parse_action_amount(opponent_action)

    player_payoff = float(TRUST_ENDOWMENT - investment + returned)
    opponent_payoff = float(investment * TRUST_MULTIPLIER - returned)
    return (player_payoff, opponent_payoff)


def _public_goods_payoff(
    player_action: str, opponent_action: str,
) -> tuple[float, float]:
    """Compute Public Goods Game payoffs.

    Each participant contributes from their endowment. The total pot is
    multiplied by (numerator / denominator) then split equally among all
    participants.
    """
    player_contrib = _parse_action_amount(player_action)
    opponent_contrib = _parse_action_amount(opponent_action)

    total_contributions = player_contrib + opponent_contrib
    multiplied_pot = (
        total_contributions * PG_MULTIPLIER_NUMERATOR / PG_MULTIPLIER_DENOMINATOR
    )
    share = multiplied_pot / PG_DEFAULT_NUM_PLAYERS

    player_payoff = float(PG_ENDOWMENT - player_contrib) + share
    opponent_payoff = float(PG_ENDOWMENT - opponent_contrib) + share
    return (player_payoff, opponent_payoff)


# ---------------------------------------------------------------------------
# Action lists for computed games
# ---------------------------------------------------------------------------

_ULTIMATUM_OFFERS: list[str] = [
    f"offer_{i}" for i in range(ULTIMATUM_POT + SINGLE_SHOT_ROUNDS)
]

_TRUST_INVESTMENTS: list[str] = [
    f"invest_{i}" for i in range(TRUST_ENDOWMENT + SINGLE_SHOT_ROUNDS)
]

_PG_CONTRIBUTIONS: list[str] = [
    f"contribute_{i}" for i in range(PG_ENDOWMENT + SINGLE_SHOT_ROUNDS)
]


# ---------------------------------------------------------------------------
# Game registry
# ---------------------------------------------------------------------------

GAME_FACTORIES: dict[str, Callable[[], GameConfig]] = {}

GAMES: dict[str, GameConfig] = {
    "prisoners_dilemma": GameConfig(
        name="Prisoner's Dilemma",
        description=(
            "Two players simultaneously choose to cooperate or defect. "
            "Mutual cooperation yields a moderate reward, mutual defection "
            "yields a low reward, and unilateral defection tempts with the "
            "highest individual payoff at the other player's expense."
        ),
        actions=["cooperate", "defect"],
        game_type="matrix",
        default_rounds=DEFAULT_NUM_ROUNDS,
        payoff_fn=_matrix_payoff_fn(_PD_MATRIX),
    ),
    "stag_hunt": GameConfig(
        name="Stag Hunt",
        description=(
            "Two players choose between hunting stag (risky but rewarding "
            "if both participate) or hunting hare (safe but less rewarding). "
            "Coordination on stag yields the highest joint payoff."
        ),
        actions=["stag", "hare"],
        game_type="matrix",
        default_rounds=DEFAULT_NUM_ROUNDS,
        payoff_fn=_matrix_payoff_fn(_SH_MATRIX),
    ),
    "hawk_dove": GameConfig(
        name="Hawk-Dove",
        description=(
            "Two players choose between aggressive (hawk) and passive (dove) "
            "strategies over a shared resource. Two hawks suffer mutual harm; "
            "a hawk facing a dove claims the resource; two doves share it."
        ),
        actions=["hawk", "dove"],
        game_type="matrix",
        default_rounds=DEFAULT_NUM_ROUNDS,
        payoff_fn=_matrix_payoff_fn(_HD_MATRIX),
    ),
    "ultimatum": GameConfig(
        name="Ultimatum Game",
        description=(
            "The proposer offers a split of a fixed pot. The responder "
            "either accepts (both receive their shares) or rejects "
            "(both receive nothing)."
        ),
        actions=_ULTIMATUM_OFFERS,
        game_type="ultimatum",
        default_rounds=SINGLE_SHOT_ROUNDS,
        payoff_fn=_ultimatum_payoff,
    ),
    "trust": GameConfig(
        name="Trust Game",
        description=(
            "The investor sends part of an endowment; the amount is "
            "multiplied and given to the trustee, who then decides how "
            "much to return."
        ),
        actions=_TRUST_INVESTMENTS,
        game_type="trust",
        default_rounds=SINGLE_SHOT_ROUNDS,
        payoff_fn=_trust_payoff,
    ),
    "public_goods": GameConfig(
        name="Public Goods Game",
        description=(
            "Each participant decides how much of their endowment to "
            "contribute to a common pool. The pool is multiplied and "
            "distributed equally, creating tension between individual "
            "free-riding and collective benefit."
        ),
        actions=_PG_CONTRIBUTIONS,
        game_type="public_goods",
        default_rounds=SINGLE_SHOT_ROUNDS,
        payoff_fn=_public_goods_payoff,
    ),
}


def get_game(name: str) -> GameConfig:
    """Retrieve a GameConfig by its registry key.

    If *name* is in :data:`GAME_FACTORIES`, the factory is called to
    produce a fresh :class:`GameConfig` with independent mutable state.
    Otherwise falls back to the static :data:`GAMES` registry.

    Args:
        name: Key in GAME_FACTORIES or GAMES.

    Returns:
        The corresponding :class:`GameConfig` instance.

    Raises:
        KeyError: If *name* is not in either registry.
    """
    if name in GAME_FACTORIES:
        return GAME_FACTORIES[name]()
    return GAMES[name]


def _load_extensions() -> None:
    """Import extension modules that register additional games."""
    import importlib
    for mod in [
        "common.games_ext.matrix_games", "common.games_ext.sequential",
        "common.games_ext.auction", "common.games_ext.nplayer",
        "common.games_ext.generated", "common.games_info.signaling",
        "common.games_info.contracts", "common.games_info.communication",
        "common.games_info.bayesian", "common.games_info.network",
        "common.games_market.oligopoly", "common.games_market.contests",
        "common.games_market.classic", "common.games_market.generated_v2",
        "common.games_market.advanced", "common.games_coop.cooperative",
        "common.games_coop.dynamic", "common.games_coop.pd_variants",
        "common.games_coop.infinite", "common.games_coop.stochastic",
        "common.meta.meta_games",
        "common.games_adaptive.factories",
    ]:
        try:
            importlib.import_module(mod)
        except ImportError:
            pass


_load_extensions()

from common.games_meta.dynamic import (  # noqa: E402,F401
    create_matrix_game, create_symmetric_game, create_custom_game,
)