from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) import random from math import floor from otree.db.models import Model, ForeignKey author = 'Christian König gen. Kersting' doc = """ Public Good Games in standard and persistent form with variable number of players and 30 + x rounds. """ class Constants(BaseConstants): name_in_url = 'pgg' players_per_group = None # we set groups dynamically below SURE_ROUNDS = 30 # make sure to update the Constants in pgg_instructions to match this value! MAX_ROUNDS = 40 # make sure to update the Constants in pgg_instructions to match this value! num_rounds = MAX_ROUNDS # we allow 10 additional rounds beyond 30 maximally FUTURE_VALUE_CALC_CUTOFF = 0.3 # stop depreciation routine if fewer tokens are left to distribute class Subsession(BaseSubsession): depreciation_rate = models.FloatField(doc="depreciation rate for players in the session") mpcr_round = models.FloatField(doc="Marginal Per Capita Return per Round") persistent = models.BooleanField(initial=False, doc="Indicator for persistent variant of pgg") group_size = models.IntegerField(initial=3, doc="Number of members per group") def creating_session(self): players = self.get_players() self.group_size = int(self.session.config.get('group_size', 3)) self.persistent = True if self.session.config.get('variant', 'standard') == 'persistent' else False if self.round_number == 1: self.depreciation_rate = self.session.vars["PERSISTENT_DEPRECIATION"] if self.persistent else \ self.session.vars["STANDARD_DEPRECIATION"] else: self.depreciation_rate = self.in_round(1).depreciation_rate self.mpcr_round = self.session.vars["MPCR_TOTAL"] * self.depreciation_rate # set treatment variables on players for easier analysis # additionally set endowments for player in players: if self.round_number == 1: if self.persistent: player.blue_token_endowment = self.session.vars["PERSISTENT_ENDOWMENT"] else: player.blue_token_endowment = self.session.vars["STANDARD_ENDOWMENT"] else: first_round_player = player.in_round(1) player.blue_token_endowment = first_round_player.blue_token_endowment # determine groups # we do this manually so that one app can handle both group sizes of 3 and 4 if self.round_number == 1: group_matrix = [] for i in range(0, len(players), self.group_size): group_matrix.append(players[i:i + self.group_size]) self.set_group_matrix(group_matrix) else: self.group_like_round(1) # determine rounds beyond 30 for group in self.get_groups(): if self.round_number == 1: additional_rounds = 0 continue_game = True while continue_game: continue_game = random.random() <= self.session.vars["CONTINUATION_PROBABILITY"] if continue_game: additional_rounds += 1 # limit to 10 additional rounds max. if additional_rounds + Constants.SURE_ROUNDS >= Constants.MAX_ROUNDS: group.num_rounds = Constants.MAX_ROUNDS else: group.num_rounds += additional_rounds else: group.num_rounds = group.in_round(1).num_rounds # add belief decision stubs for player in players: if self.round_number in [1, 11, 21, 30]: player.generate_belief_stubs(self.round_number) class Group(BaseGroup): num_rounds = models.IntegerField(initial=30) total_contributions = models.FloatField(doc="sum of contributions in this round") pot_size = models.FloatField(doc="sum of contributions + carry over in this round") orange_tokens_generated = models.FloatField(doc="amount of orange tokens generated from pot size in this round") amount_to_carry_over = models.FloatField(doc="carry over amount of blue tokens to next round") amount_to_distribute = models.FloatField(doc="amount to distribute equally among group members in this round") def prepare_round(self): if self.round_number > 1: for player in self.get_players(): player.orange_tokens = player.in_round(self.round_number - 1).orange_tokens self.pot_size = self.in_round(self.round_number - 1).amount_to_carry_over if self.round_number == 1 or self.subsession.depreciation_rate == 1: self.pot_size = 0 def handle_contributions(self): """Run after each contribution round, handles all PG related calculations""" players = self.get_players() num_players = len(players) self.total_contributions = sum([player.blue_token_contribution for player in players]) self.pot_size += self.total_contributions self.orange_tokens_generated = self.pot_size * (self.subsession.mpcr_round * num_players) # we want integer amounts that work with the group size! # check how often it fits in, then distribute right amount # self.amount_to_distribute = int((self.orange_tokens_generated // num_players) * num_players) # self.amount_to_carry_over = floor(self.pot_size * self.subsession.depreciation_rate) # individual_share = int(self.amount_to_distribute / num_players) # giving up integer restriction: self.amount_to_distribute = self.orange_tokens_generated self.amount_to_carry_over = self.pot_size * self.subsession.depreciation_rate individual_share = self.amount_to_distribute / num_players # add tokens to players and exchange remaining blue tokens into orange ones for player in players: player.pgg_payoff = individual_share if self.round_number == 1: player.orange_tokens = individual_share else: player.orange_tokens += individual_share player.exchange_blue_tokens() if self.round_number == self.num_rounds: if self.subsession.persistent: future_value = self._future_value(self.amount_to_carry_over) for player in players: if self.subsession.persistent: player.orange_tokens += future_value player.participant.vars["blue_tokens_in_pot"] = self.amount_to_carry_over player.participant.vars["remainder_orange_tokens"] = future_value player.participant.vars["sum_orange_tokens"] = player.orange_tokens player.exchange_orange_tokens() def _future_value(self, blue_tokens): group_size = len(self.get_players()) orange_tokens = 0 working_tokens = blue_tokens while True: generated_tokens = working_tokens * (self.subsession.mpcr_round * group_size) if generated_tokens < Constants.FUTURE_VALUE_CALC_CUTOFF: break orange_tokens += generated_tokens working_tokens = working_tokens * self.subsession.depreciation_rate return int(orange_tokens // group_size) class Player(BasePlayer): # tokens blue_token_endowment = models.IntegerField(doc="endowment with blue tokens for each round") blue_token_contribution = models.IntegerField(doc="contribution of blue tokens to the pg", min=0) pgg_payoff = models.FloatField(doc="payoff from public good in orange tokens") orange_tokens = models.FloatField(initial=0, doc="number of orange tokens accumulated") def exchange_blue_tokens(self): """Exchange blue tokens into orange tokens""" self.orange_tokens += (self.blue_token_endowment - self.blue_token_contribution) * self.session.vars[ "ORANGE_TOKENS_PER_BLUE_TOKEN"] def exchange_orange_tokens(self): """Exchange orange tokens into EUR""" self.payoff = c(self.orange_tokens * self.session.vars.get("EUR_PER_ORANGE_TOKEN", 1)) def blue_token_contribution_max(self): return self.session.vars["PERSISTENT_ENDOWMENT"] if self.subsession.persistent else self.session.vars[ "STANDARD_ENDOWMENT"] def generate_belief_stubs(self, round_number): num_players = len(self.group.get_players()) player_ids = range(1, num_players + 1) other_player_ids = [id for id in player_ids if id != self.id_in_group] # Belief elicitation in rounds 1, 11, and 21 takes place before decisions are made. Thus, they refer to behavior # in the first, 11th, and 21st round. # Due to the way we handle belief elicitation pages, the fourth belief question that refers to round 31 # technically takes place in round 30 (because round 31 may not exist), but after the decision for round 30 # has already been made. To avoid confusion, we store it as being in round 31, store_round = round_number + 1 if round_number == Constants.SURE_ROUNDS else round_number for other_player_id in other_player_ids: belief = self.belief_set.create() belief.session_code = self.session.code belief.participant_code = self.participant.code belief.round = store_round belief.player_in_group = self.id_in_group belief.about_player_in_group = other_player_id belief.save() class Belief(Model): player = ForeignKey(Player, on_delete=models.CASCADE) session_code = models.StringField() participant_code = models.StringField() player_in_group = models.IntegerField() about_player_in_group = models.IntegerField() value = models.IntegerField(blank=True) round = models.IntegerField()