"""N-player social dilemma games for KantBench. Modeled as one agent vs one opponent (representing aggregate of others). """ from __future__ import annotations from common.games import GAMES, GameConfig from constant_definitions.game_constants import DEFAULT_NUM_ROUNDS, SINGLE_SHOT_ROUNDS from constant_definitions.auction_nplayer_constants import ( COMMONS_RESOURCE_CAPACITY, COMMONS_MAX_EXTRACTION, COMMONS_DEPLETION_PENALTY, VOLUNTEER_BENEFIT, VOLUNTEER_COST, VOLUNTEER_NO_VOL, EL_FAROL_ATTEND_REWARD, EL_FAROL_CROWD_PENALTY, EL_FAROL_STAY_HOME, EL_FAROL_CAPACITY, ) _ONE = int(bool(True)) _ZERO_F = float() # -- Tragedy of the Commons -- def _commons_payoff( player_action: str, opponent_action: str, ) -> tuple[float, float]: """Resource extraction game. Each player extracts from a shared resource. If total extraction exceeds capacity, both suffer a depletion penalty. """ p_extract = int(player_action.rsplit("_", _ONE)[_ONE]) o_extract = int(opponent_action.rsplit("_", _ONE)[_ONE]) total = p_extract + o_extract if total > COMMONS_RESOURCE_CAPACITY: return (float(COMMONS_DEPLETION_PENALTY), float(COMMONS_DEPLETION_PENALTY)) return (float(p_extract), float(o_extract)) _COMMONS_ACTIONS = [ f"extract_{i}" for i in range(COMMONS_MAX_EXTRACTION + _ONE) ] # -- Volunteer's Dilemma -- def _volunteer_payoff( player_action: str, opponent_action: str, ) -> tuple[float, float]: """At least one must volunteer for everyone to benefit. Volunteering costs the volunteer but benefits all. If nobody volunteers, everyone gets nothing. """ p_vol = player_action == "volunteer" o_vol = opponent_action == "volunteer" if not p_vol and not o_vol: return (float(VOLUNTEER_NO_VOL), float(VOLUNTEER_NO_VOL)) p_pay = float(VOLUNTEER_BENEFIT - VOLUNTEER_COST) if p_vol else float(VOLUNTEER_BENEFIT) o_pay = float(VOLUNTEER_BENEFIT - VOLUNTEER_COST) if o_vol else float(VOLUNTEER_BENEFIT) return (p_pay, o_pay) # -- El Farol Bar Problem -- def _el_farol_payoff( player_action: str, opponent_action: str, ) -> tuple[float, float]: """Bar attendance decision game. Going to the bar is fun if few attend (under capacity), but unpleasant if crowded. Staying home gives a moderate fixed payoff. """ p_goes = player_action == "attend" o_goes = opponent_action == "attend" attendees = int(p_goes) + int(o_goes) crowded = attendees > _ONE if not p_goes: p_pay = float(EL_FAROL_STAY_HOME) elif crowded: p_pay = float(EL_FAROL_CROWD_PENALTY) else: p_pay = float(EL_FAROL_ATTEND_REWARD) if not o_goes: o_pay = float(EL_FAROL_STAY_HOME) elif crowded: o_pay = float(EL_FAROL_CROWD_PENALTY) else: o_pay = float(EL_FAROL_ATTEND_REWARD) return (p_pay, o_pay) # -- Register -- NPLAYER_GAMES: dict[str, GameConfig] = { "tragedy_of_commons": GameConfig( name="Tragedy of the Commons", description=( "Players extract resources from a shared pool. Individual " "incentive is to extract more, but if total extraction exceeds " "the sustainable capacity, the resource collapses and everyone " "suffers. Models environmental and resource management dilemmas." ), actions=_COMMONS_ACTIONS, game_type="commons", default_rounds=DEFAULT_NUM_ROUNDS, payoff_fn=_commons_payoff, ), "volunteer_dilemma": GameConfig( name="Volunteer's Dilemma", description=( "At least one player must volunteer (at personal cost) for " "everyone to receive a benefit. If nobody volunteers, all get " "nothing. Models bystander effects and public good provision." ), actions=["volunteer", "abstain"], game_type="matrix", default_rounds=DEFAULT_NUM_ROUNDS, payoff_fn=_volunteer_payoff, ), "el_farol": GameConfig( name="El Farol Bar Problem", description=( "Each player decides whether to attend a bar. If attendance " "is below capacity, going is better than staying home. If the " "bar is crowded, staying home is better. Models minority games " "and congestion dynamics." ), actions=["attend", "stay_home"], game_type="matrix", default_rounds=DEFAULT_NUM_ROUNDS, payoff_fn=_el_farol_payoff, ), } GAMES.update(NPLAYER_GAMES)