| | """Sequential (extensive-form) games for KantBench.""" |
| | from __future__ import annotations |
| |
|
| | from common.games import GAMES, GameConfig |
| | from constant_definitions.game_constants import SINGLE_SHOT_ROUNDS, DEFAULT_NUM_ROUNDS |
| | from constant_definitions.sequential_constants import ( |
| | DICTATOR_ENDOWMENT, |
| | CENTIPEDE_INITIAL_POT, CENTIPEDE_GROWTH_MULTIPLIER, CENTIPEDE_MAX_STAGES, |
| | CENTIPEDE_LARGE_SHARE_NUMERATOR, CENTIPEDE_LARGE_SHARE_DENOMINATOR, |
| | CENTIPEDE_SMALL_SHARE_NUMERATOR, CENTIPEDE_SMALL_SHARE_DENOMINATOR, |
| | STACKELBERG_DEMAND_INTERCEPT, STACKELBERG_DEMAND_SLOPE, |
| | STACKELBERG_MARGINAL_COST, STACKELBERG_MAX_QUANTITY, |
| | ) |
| |
|
| | _ONE = int(bool(True)) |
| |
|
| |
|
| | |
| |
|
| | def _dictator_payoff(player_action: str, opponent_action: str) -> tuple[float, float]: |
| | """Dictator allocates from endowment; recipient has no choice.""" |
| | amount = int(player_action.rsplit("_", _ONE)[_ONE]) |
| | dictator_keeps = float(DICTATOR_ENDOWMENT - amount) |
| | recipient_gets = float(amount) |
| | return (dictator_keeps, recipient_gets) |
| |
|
| |
|
| | _DICTATOR_ACTIONS = [ |
| | f"give_{i}" for i in range(DICTATOR_ENDOWMENT + _ONE) |
| | ] |
| |
|
| |
|
| | |
| |
|
| | def _centipede_payoff(player_action: str, opponent_action: str) -> tuple[float, float]: |
| | """Alternating pass/take game with growing pot. |
| | |
| | Actions encode the stage: 'take_N' means take at stage N, |
| | 'pass_all' means pass through all stages. |
| | The opponent strategy similarly responds with take or pass. |
| | """ |
| | if player_action == "pass_all": |
| | player_stage = CENTIPEDE_MAX_STAGES + _ONE |
| | else: |
| | player_stage = int(player_action.rsplit("_", _ONE)[_ONE]) |
| |
|
| | if opponent_action == "pass_all": |
| | opp_stage = CENTIPEDE_MAX_STAGES + _ONE |
| | else: |
| | opp_stage = int(opponent_action.rsplit("_", _ONE)[_ONE]) |
| |
|
| | take_stage = min(player_stage, opp_stage) |
| |
|
| | pot = CENTIPEDE_INITIAL_POT |
| | for _ in range(take_stage): |
| | pot = pot * CENTIPEDE_GROWTH_MULTIPLIER |
| |
|
| | large = pot * CENTIPEDE_LARGE_SHARE_NUMERATOR // CENTIPEDE_LARGE_SHARE_DENOMINATOR |
| | small = pot * CENTIPEDE_SMALL_SHARE_NUMERATOR // CENTIPEDE_SMALL_SHARE_DENOMINATOR |
| |
|
| | if player_stage <= opp_stage: |
| | return (float(large), float(small)) |
| | return (float(small), float(large)) |
| |
|
| |
|
| | _CENTIPEDE_ACTIONS = [ |
| | f"take_{i}" for i in range(CENTIPEDE_MAX_STAGES + _ONE) |
| | ] + ["pass_all"] |
| |
|
| |
|
| | |
| |
|
| | def _stackelberg_payoff( |
| | player_action: str, opponent_action: str, |
| | ) -> tuple[float, float]: |
| | """Stackelberg duopoly: leader (player) and follower (opponent). |
| | |
| | Profit = (demand_intercept - slope * (q_leader + q_follower) - cost) * q |
| | """ |
| | q_leader = int(player_action.rsplit("_", _ONE)[_ONE]) |
| | q_follower = int(opponent_action.rsplit("_", _ONE)[_ONE]) |
| |
|
| | total_q = q_leader + q_follower |
| | price = STACKELBERG_DEMAND_INTERCEPT - STACKELBERG_DEMAND_SLOPE * total_q |
| |
|
| | leader_profit = float((price - STACKELBERG_MARGINAL_COST) * q_leader) |
| | follower_profit = float((price - STACKELBERG_MARGINAL_COST) * q_follower) |
| | return (leader_profit, follower_profit) |
| |
|
| |
|
| | _STACKELBERG_ACTIONS = [ |
| | f"produce_{i}" for i in range(STACKELBERG_MAX_QUANTITY + _ONE) |
| | ] |
| |
|
| |
|
| | |
| |
|
| | SEQUENTIAL_GAMES: dict[str, GameConfig] = { |
| | "dictator": GameConfig( |
| | name="Dictator Game", |
| | description=( |
| | "One player (the dictator) decides how to split an endowment " |
| | "with a passive recipient who has no say. Tests fairness " |
| | "preferences and altruistic behavior when there is no strategic " |
| | "incentive to share." |
| | ), |
| | actions=_DICTATOR_ACTIONS, |
| | game_type="dictator", |
| | default_rounds=SINGLE_SHOT_ROUNDS, |
| | payoff_fn=_dictator_payoff, |
| | ), |
| | "centipede": GameConfig( |
| | name="Centipede Game", |
| | description=( |
| | "Players alternate deciding to take or pass. Each pass doubles " |
| | "the pot. The taker gets the larger share while the other gets " |
| | "the smaller share. Backward induction predicts immediate taking, " |
| | "but cooperation through passing yields higher joint payoffs." |
| | ), |
| | actions=_CENTIPEDE_ACTIONS, |
| | game_type="centipede", |
| | default_rounds=SINGLE_SHOT_ROUNDS, |
| | payoff_fn=_centipede_payoff, |
| | ), |
| | "stackelberg": GameConfig( |
| | name="Stackelberg Competition", |
| | description=( |
| | "A quantity-setting duopoly where the leader commits to a " |
| | "production quantity first, and the follower observes and " |
| | "responds. The leader can exploit first-mover advantage. " |
| | "Price is determined by total market quantity." |
| | ), |
| | actions=_STACKELBERG_ACTIONS, |
| | game_type="stackelberg", |
| | default_rounds=DEFAULT_NUM_ROUNDS, |
| | payoff_fn=_stackelberg_payoff, |
| | ), |
| | } |
| |
|
| | GAMES.update(SEQUENTIAL_GAMES) |
| |
|