from otree.api import * import numpy as np import random c = cu def random_termination(continue_prob, blocklength): """ randomly generates the number of rounds that are payoff relevant according to a geometric distribution and computes the rounds that a player has to complete according to length of blocks continue_prob: (float) probability of entering another round (1-prob of success) blocklength: (int) number of rounds in each block return: total rounds played (int), number of rounds that count for payoff (int) """ stop_prob = 1-continue_prob #simple eror avoidance if stop_prob<=0 or stop_prob>1: print("Probability error - continuation prob outside range") stop_prob=1 rndVar = np.random.default_rng() counted_rounds = rndVar.geometric(stop_prob) if counted_rounds % blocklength > 0: nextBlock=1 else: nextBlock=0 roundsPlayed = int(counted_rounds/blocklength)*blocklength+nextBlock*blocklength return 3,1 #roundsPlayed,counted_rounds def payoff_relevance(current_round, payoff_rounds): """ Determines if current round is payoff relevant current_round: (int) number of current round payoff_rounds: (int) number of rounds relevant for payoff """ if current_round <= payoff_rounds: return 1 else: return 0 doc = 'Each player decides if they contribute anything from their endowment to the public good. The public good is split equally among everyone.' class Constants(BaseConstants): name_in_url = 'public_goods' blocklength = 15 #random block termination: length of block continuation_prob = 0.85 #discount factor delta; must be in [0,1) players_per_group = 3 num_rounds=5*blocklength #overall maximum of rounds after which game ends for sure multiplier = 2 #public goods productivity endowment= [10,10,10] #endowment under equality #endowment multipliers written out for readability #Attention: must be such that endowment*treatment gives integer values, otherwise decimals are lost endowment_treat0 = [1,1,1] endowment_treat1 = [1/2,1,2] currency_conversion = 0.1 # manual currency conversion if internal currency fields are not used for input, multiplied with points endowment_treats={0:endowment_treat0, 1:endowment_treat1} #endowment multipliers for each treatment, 0 being equality #explicit definition necessary for otree - recognizes _role player1_role = 'Player 1' player2_role = 'Player 2' player3_role = 'Player 3' player_roles = [player1_role, player2_role, player3_role] class Subsession(BaseSubsession): pass #In the first subsession, participants in a group get assigned the categorical or flexible treatment #the data field is stored on the participant level as creating_session is executed at every subsession def creating_session(subsession): if subsession.round_number == 1: #set treatment variables and game length (on group level) for group in subsession.get_groups(): categorical_var = random.choice([0,1]) endowment_var = random.choice([0,1]) #set endowment multiplier depending on endowment treatment endow_multiplier = Constants.endowment_treats[endowment_var] #print(endowment_var) #random block termination length_constants = random_termination(Constants.continuation_prob, Constants.blocklength) #print(group, categorical_var) for player in group.get_players(): #assign treatment vars to participants (only in 1st round) participant = player.participant participant.categorical_set = categorical_var participant.endowment_treatment = endowment_var participant.rounds_total = length_constants[0] participant.rounds_payoff = length_constants[1] for i in range(len(Constants.player_roles)): if player.role==Constants.player_roles[i]: participant.endowment = int(Constants.endowment[i]*endow_multiplier[i]) for player in subsession.get_players(): participant = player.participant #determines if current round is payoff relevant and sets variable #executed in every round player.payoff_relevant = payoff_relevance(subsession.round_number, participant.rounds_payoff) def set_payoffs(group): for player in group.get_players(): participant = player.participant if participant.categorical_set == 1: player.contribution = player.contribCat else: player.contribution = player.contribFlex group.total_contribution += player.contribution #each player's share in the public good group.individual_share = (group.total_contribution * Constants.multiplier / Constants.players_per_group) for player in group.get_players(): participant = player.participant #computation of actual payoffs, taking into account whether game has already ended (used for payout) player.payoff = (player.payoff_relevant * (participant.endowment - player.contribution + group.individual_share))*Constants.currency_conversion #computation of nominal payoffs, not taking into account whether game has already ended (used for display) player.nominal_payoff = float(participant.endowment - player.contribution + group.individual_share) def return_treatment_endowments(player): ''' Computes endowment vector based on treatment status of player (same across group) parameter: player returns: endowment vector as list ''' participant = player.participant endow_vec=[] endowment_multi = Constants.endowment_treats[participant.endowment_treatment] for i in range(len(endowment_multi)): endow_vec.append(int(Constants.endowment[i]*endowment_multi[i])) return endow_vec def return_contributions(player): ''' return contribution vector for current round ''' contrib=[] group = player.group for i in Constants.player_roles: player1 = group.get_player_by_role(i) contrib.append(player1.contribution) return contrib class Group(BaseGroup): total_contribution = models.IntegerField(initial=0) individual_share = models.FloatField() set_payoffs = set_payoffs def contribFlex_max(player): participant = player.participant return int(participant.endowment) def contribCat_choices(player): participant = player.participant return [0, int(participant.endowment)] class Player(BasePlayer): contribution = models.IntegerField() #records if current round is payoff relevant payoff_relevant = models.IntegerField() #stores nominal payoff which does not take into account whether the game has already ended nominal_payoff = models.FloatField() #Data fields for categorical treatment contribCat = models.IntegerField(label='How much do you contribute') #needs to be integer fields, as default submission is string otherwise contribCat_choices = contribCat_choices #Data fields for flex treatment contribFlex = models.IntegerField(label='How much do you contribute', min=0) contribFlex_max = contribFlex_max #flexible display of data entry fields depending on treatment status @staticmethod def get_form_fields(player): participant = player.participant if participant.categorical_set==1: return ['contribCat'] else: return ['contribFlex'] class Contribute(Page): form_model = 'player' @staticmethod def vars_for_template(player): #makes endowment variables accessible on Contribution page endow = return_treatment_endowments(player) return dict( endowment1=endow[0], endowment2=endow[1], endowment3=endow[2], ) #Display entry field depending on whether we are in flexible (categorical_set=0) or categorical mode (categorical_set=1) get_form_fields = get_form_fields timeout_seconds = 10 class FirstWaitPage(WaitPage): body_text = "Waiting for players to arrive" group_by_arrival_time = True @staticmethod def is_displayed(player): return player.round_number >1 class ContributeWaitPage(WaitPage): @staticmethod def is_displayed(player): return player.round_number == 1 class ResultsWaitPage(WaitPage): after_all_players_arrive = 'set_payoffs' def points_display_rounding(exact_value): ''' Converts numerical value into displayable points value: float without decimal places -> int; float with decimals -> rounded to 2 places ''' if exact_value > 0: if exact_value % 1 == 0: display_value =int(exact_value) else: display_value =round(exact_value,2) else: display_value = 0 return display_value class Results(Page): form_model = 'player' timeout_seconds = 180 @staticmethod def js_vars(player): endow = return_treatment_endowments(player) contrib = return_contributions(player) retained_endow = [] for i in range(len(endow)): retained_endow.append(endow[i]-contrib[i]) return dict( contribution1=contrib[0], contribution2=contrib[1], contribution3=contrib[2], ret1=retained_endow[0], ret2=retained_endow[1], ret3=retained_endow[2] ) @staticmethod def vars_for_template(player): endow = return_treatment_endowments(player) contrib = return_contributions(player) contrib_fraction = [] for i in range(len(endow)): contrib_fraction.append(round(contrib[i]/endow[i]*100,1)) group=player.group participant = player.participant pg_rounded = points_display_rounding(group.individual_share*Constants.players_per_group) return dict( endowment1=int(endow[0]), endowment2=int(endow[1]), endowment3=int(endow[2]), contribution1=int(contrib[0]), contribution2=int(contrib[1]), contribution3=int(contrib[2]), fraction1 = contrib_fraction[0], fraction2 = contrib_fraction[1], fraction3 = contrib_fraction[2], pg = pg_rounded, pg_share = points_display_rounding(group.individual_share), ret_endow = points_display_rounding(participant.endowment - player.contribution), display_payoff = points_display_rounding(player.nominal_payoff) ) class Block(Page): #summary of current block, only displayed if game continues beyond block #ADJUST CONDITION TO PROPER TEST> <= to < and 3 to blocklength @staticmethod def is_displayed(player): participant = player.participant return ((player.round_number > 1) and (player.round_number % 3 == 0)) and (player.round_number <= participant.rounds_total) form_model = 'player' timeout_seconds = 60 class End(Page): #is only shown when game has ended - either after a block or when it reaches the max round condition def is_displayed(player): participant = player.participant return (player.round_number == participant.rounds_total) or (player.round_number == Constants.num_rounds) form_model = 'player' @staticmethod def app_after_this_page(player, upcoming_apps): return 'payment' page_sequence = [FirstWaitPage, ContributeWaitPage, Contribute, ResultsWaitPage, Results, Block, End]