from otree.api import * from decimal import * doc = """ The actual game: after 20 rounds randomisation until round 30. """ class C(BaseConstants): NAME_IN_URL = 'the_game' PLAYERS_PER_GROUP = 5 NUM_ROUNDS = 30 # will be randomly terminated between 20 and 30 ENDOWMENT = 10 MULTIPLIER = 3 INSTRUCT_TEMPLATE = 'ChoiceGame/instructions.html' PROBABILITY_END = 50 # the probability to end is 100 - the Constant (50) ROUND_END = 20 SELECTED_ROUNDS = 5 EXCHANGE_RATE = 0.1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session session.treat = session.config['treat'] for player in subsession.get_players(): player.participant.income_game = 0 player.participant.is_dropout = False class Group(BaseGroup): total_contribution = models.CurrencyField(doc="""Group: How much was contributed to group account""") individual_share = models.FloatField(doc="""Group: individual return from group account""") embezzlement = models.CurrencyField(doc="""Group: embezzlement by Participant B""") comm_action = models.CurrencyField(doc="""Group: communicated embezzlement level""") comm_contribution = models.CurrencyField(doc="""Group: communicated contribution level""") communication_choice = models.IntegerField( doc="""Group: Choice of communication option""", initial=999 ) number_in_round = models.IntegerField(doc="""Group: random number drawn from round 21 to determine end of game""") number_of_dropouts = models.IntegerField(initial=0) dictator_out = models.BooleanField(initial=False) class Player(BasePlayer): # For Citizens # ------------------------------------------------------------------------------------- contribution = models.CurrencyField( doc="""Player: individual contribution to group account by participant of Type A""", label="How many Points do you want to transfer to the group account?", min=cu(0), max=C.ENDOWMENT ) # type = models.StringField( # doc="""Player: assignment of Types to players""" # ) belief_contribution = models.CurrencyField( doc="""Player: assignment of Types to players""", label="How many Points were contributed to the group account by all players of type A (including you)?", min=cu(0), max=cu(40) ) belief_dictator = models.CurrencyField( label="What do you think is the total number of Points in the group account after player B's decision?", min=cu(-40), max=cu(10) ) # Variables for Belief Elicitation: belief_diff_group = models.CurrencyField( ) belief_pay_group = models.BooleanField( ) belief_diff_dic = models.CurrencyField( ) belief_pay_dic = models.BooleanField( ) belief_pay = models.IntegerField(initial=0 ) t_out_contr = models.BooleanField(initial=False) t_out_beliefs = models.BooleanField(initial=False) # For Dictator # ------------------------------------------------------------------------------------- dictator_action = models.CurrencyField( # needs to be adjusted to have dynamic bounds + define whether it is positive or negative doc="""Amount dictator contribute (positive) or embezzle (negative) from group account""", max=C.ENDOWMENT, # min is dynamically determined by the function dictator_action_min ) communicated_action = models.CurrencyField( # the action that will be communicated by the dictator to the public label="What action do you want to communicate to the group?", min=cu(-40), max=cu(10), blank=True, ) # Choice Field for which Communication to communicate # I then think it's easiest to just define three different Result pages and condition it on this decision. # To have it available for all players it must be on the group level. dictator_choice = models.IntegerField( label="Please choose which type of communication you want to choose.", widget=widgets.RadioSelect, choices=[ [1, 'Communicate No Further Information'], [2, 'Communicate True Combination'], [3, 'Communicate Yourself'], ] ) t_out_action = models.BooleanField(initial=False) t_out_choice = models.BooleanField(initial=False) t_out_comm = models.BooleanField(initial=False) # For Both # ------------------------------------------------------------------------------------- # Setting default of finished rounds to False which might be algorithmically changes float_payoff = models.FloatField() finished_rounds = models.BooleanField(initial=False) timeout_count = models.IntegerField(initial=0) is_dropout = models.BooleanField(initial=False) # Results fields selected_rounds_game = models.LongStringField(initial='[]') selected_game_income = models.LongStringField(initial='[]') selected_rounds_beliefs = models.LongStringField(initial='[]') selected_beliefs_income = models.LongStringField(initial='[]') total_income_decisions = models.FloatField() total_income_beliefs = models.CurrencyField(initial=0) total_income = models.FloatField() # ------------------------------------------------------------------------------------------- # # FUNCTIONS # # ------------------------------------------------------------------------------------------- # # Dynamically determining the maximum that a dictator can embezzle. def dictator_action_min(player): group = player.group return -group.total_contribution def role_assignment(group): if group.round_number == 1: players = group.get_players() num_players = len(players) if num_players == C.PLAYERS_PER_GROUP: # Shuffle the players import random random.shuffle(players) # Assign the first four players as "Citizen" for i in range(num_players - 1): players[i].participant.type = "Citizen" # Assign the last player as "Dictator" players[-1].participant.type = "Dictator" # else: # players = group.get_players() # num_players = len(players) # for i in range(num_players): # players[i].type = players[i].in_round(1).type def aggregate_contributions(group): players = group.get_players() citizens = [p for p in players if p.participant.type == 'Citizen'] contributions = [p.contribution for p in citizens] group.total_contribution = sum(contributions) def set_payoffs(group): players = group.get_players() # Converting Embezzlement to Group Variable citizens = [p for p in players if p.participant.type == 'Citizen'] dictator = [p for p in players if p.participant.type == 'Dictator'] embezzlement = [-p.dictator_action for p in dictator] group.embezzlement = sum(embezzlement) # Return from Public Good for all players in Round group.individual_share = (float((group.total_contribution - group.embezzlement)) * C.MULTIPLIER) / C.PLAYERS_PER_GROUP # Define income from game for citizens and the dictator for p in citizens: p.payoff = C.ENDOWMENT - p.contribution + group.individual_share p.float_payoff = float(C.ENDOWMENT - p.contribution) + group.individual_share # Define payoff for Belief elicitation. # Belief on group contribution p.belief_diff_group = p.belief_contribution - p.group.total_contribution p.belief_pay_group = abs(int(p.belief_diff_group)) < 3 # Belief on dictator contribution p.belief_diff_dic = p.belief_dictator + p.group.embezzlement p.belief_pay_dic = abs(int(p.belief_diff_dic)) < 3 # Belief correct on both dimensions? p.belief_pay = ((p.belief_pay_group + p.belief_pay_dic) == 2)*3 for p in dictator: p.payoff = C.ENDOWMENT - p.dictator_action + group.individual_share p.float_payoff = float(C.ENDOWMENT - p.dictator_action) + group.individual_share # Define Communication Choice on Group Level def choice_of_communication(group): players = group.get_players() dictator = [p for p in players if p.participant.type == 'Dictator'] temp = [p.dictator_choice for p in dictator] group.communication_choice = sum(temp) # Define communicated Levels on Group Level def communicated_levels(group): players = group.get_players() dictator = [p for p in players if p.participant.type == 'Dictator'] if group.communication_choice == 3 or group.session.treat == 3: c_action = [p.communicated_action for p in dictator] group.comm_action = sum(c_action) group.comm_contribution = group.total_contribution - group.embezzlement - group.comm_action # Define Rules to end the Game after 20 rounds def last_round_determination(group): import random if group.round_number > C.ROUND_END: group.number_in_round = random.randint(0, 100) if group.round_number > C.ROUND_END and group.number_in_round > C.PROBABILITY_END: print('ending game') for p in group.get_players(): p.finished_rounds = True # Define Selection of Payout Relevant Rounds def final_payoff(group): import random players = group.get_players() for p in players: if p.finished_rounds or (group.number_of_dropouts >= 3 and p.round_number >= C.SELECTED_ROUNDS): # select rounds and then add together... upper_bound = p.round_number + 1 selected_game_rounds = random.sample(range(1, upper_bound), C.SELECTED_ROUNDS) selected_belief_rounds = random.sample(range(1, upper_bound), C.SELECTED_ROUNDS) # Get the relevant incomes/payoffs of the selected rounds selected_income = 0 selected_beliefs = 0 selected_round_incomes = [] selected_round_beliefs = [] for s_round in selected_game_rounds: round_income = p.in_round(s_round).float_payoff selected_round_incomes.append(round_income) # Store the payoff for the selected round selected_income += round_income print(selected_income) # for each s_round I want to save it to a numerated player variable... for s_round in selected_belief_rounds: round_beliefs = p.in_round(s_round).belief_pay selected_round_beliefs.append(round_beliefs) selected_beliefs += round_beliefs print(selected_beliefs) # save the selected rounds and corresponding payoffs as player variables p.selected_rounds_game = ', '. join(str(e) for e in selected_game_rounds) print(p.selected_rounds_game) p.selected_rounds_beliefs = ', '. join(str(e) for e in selected_belief_rounds) print(p.selected_rounds_beliefs) p.selected_game_income = ', '. join(str(e) for e in selected_round_incomes) print(p.selected_game_income) p.selected_beliefs_income = ', '. join(str(e) for e in selected_round_beliefs) print(p.selected_beliefs_income) p.total_income_decisions = selected_income p.total_income_beliefs = selected_beliefs p.total_income = p.total_income_decisions + float(p.total_income_beliefs) # on participant level: p.participant.income_decisions = p.total_income_decisions p.participant.income_beliefs = p.total_income_beliefs p.participant.income_game = p.total_income # Define Function to exclude inactive players: def exclude_inactive(player): # determine whether individual player is excluded if player.timeout_count == 3: player.participant.is_dropout = True player.is_dropout = True # Make sure when know if dictator dropped out if player.participant.type == 'Dictator': player.group.dictator_out = True # record how many participants are excluded players = player.group.get_players() drop_outs = [p for p in players if p.participant.is_dropout] player.group.number_of_dropouts = len(drop_outs) # ------------------------------------------------------------------------------------------- # # PAGES # # ------------------------------------------------------------------------------------------- # class GroupWaitPage(WaitPage): @staticmethod def is_displayed(player): return player.round_number == 1 group_by_arrival_time = True after_all_players_arrive = role_assignment title_text = "Thanks for joining" body_text = "Please wait for all other members to join" class RolePage(Page): @staticmethod def is_displayed(player): return player.round_number == 1 @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 15 @staticmethod def vars_for_template(player): parb = player.participant.type == "Dictator" para = player.participant.type == "Citizen" return dict( parB=parb, parA=para ) # ------------------------ CITIZEN PAGES -------------------------- # class CitizenPage(Page): @staticmethod def is_displayed(player): return player.participant.type == "Citizen" form_model = 'player' form_fields = ['contribution'] @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: if player.round_number == 1: return 40 else: return 30 @staticmethod def before_next_page(player, timeout_happened): # Get the number of time-outs from last round if player.round_number > 1: last_round = player.round_number - 1 player.timeout_count = player.in_round(last_round).timeout_count # Implement the rules for contribution when player timed out if timeout_happened and not player.participant.is_dropout: player.t_out_contr = True # Calculate whether player should be excluded from game player.timeout_count += 1 # Exclusion criterion exclude_inactive(player) # Implement rule for when people dropped out if timeout_happened and not player.participant.is_dropout: if player.round_number == 1: player.contribution = 0 else: player.contribution = player.in_round(player.round_number - 1).contribution elif timeout_happened and player.participant.is_dropout: player.t_out_contr = True last_round = player.round_number - 1 last_player = player.in_round(last_round) player.contribution = round(1/3 * last_player.group.individual_share, 0) else: player.timeout_count = 0 class DictatorWaitPage(WaitPage): after_all_players_arrive = aggregate_contributions body_text = "Please wait for all Participants of Type A to make their transfer decision." class CitizenBeliefs(Page): @staticmethod def is_displayed(player): return player.participant.type == "Citizen" form_model = 'player' form_fields = ['belief_contribution', 'belief_dictator'] @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 40 @staticmethod def before_next_page(player, timeout_happened): if timeout_happened: player.t_out_beliefs = True player.belief_contribution = 0 player.belief_dictator = 0 # Calculate whether player should be excluded from game player.timeout_count += 1 else: player.timeout_count = 0 exclude_inactive(player) # ------------------------ DICTATOR PAGES -------------------------- # class DictatorPage(Page): @staticmethod def is_displayed(player): return player.participant.type == "Dictator" form_model = 'player' form_fields = ['dictator_action'] @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 40 @staticmethod def js_vars(player: Player): return dict(potsize=player.group.total_contribution, endowment=C.ENDOWMENT) @staticmethod def vars_for_template(player): maxpot = player.group.total_contribution return dict( min=-int(maxpot), potsize=int(maxpot) ) @staticmethod def before_next_page(player, timeout_happened): # Get the number of time-outs from last round if player.round_number > 1: last_round = player.round_number - 1 last_player = player.in_round(last_round) player.timeout_count = last_player.timeout_count # Variable for defining action if timeout happened if timeout_happened: last_contribution = last_player.group.total_contribution last_action = last_player.dictator_action # Implement the rules for contribution when player timed out if timeout_happened and not player.participant.is_dropout: player.t_out_action = True # Calculate whether player should be excluded from game player.timeout_count += 1 exclude_inactive(player) if timeout_happened and not player.participant.is_dropout: if player.round_number == 1: player.dictator_action = 0 elif last_contribution == 0: player.dictator_action = last_action else: last_action_share = int(last_action)/int(last_contribution) action_share = round(last_action_share * int(player.group.total_contribution), 0) player.dictator_action = action_share # Calculate whether player should be excluded from game player.timeout_count += 1 elif timeout_happened and player.participant.is_dropout: player.t_out_action = True player.dictator_action = round(1/3 * last_player.group.individual_share, 0) else: player.timeout_count = 0 # @staticmethod # def app_after_this_page(player, upcoming_apps): # if player.participant.is_dropout: # return upcoming_apps[0] class DictatorChoice(Page): @staticmethod def is_displayed(player): return player.participant.type == "Dictator" and player.session.treat == 4 form_model = 'player' form_fields = ['dictator_choice', 'communicated_action'] @staticmethod def vars_for_template(player): realpot = player.group.total_contribution + player.dictator_action return dict( max_contr=min(int(realpot), 10), max_embezzle=int(realpot)-10*(C.PLAYERS_PER_GROUP-1), realcontr=int(player.group.total_contribution), realaction=int(player.dictator_action) ) @staticmethod def js_vars(player: Player): return dict(real_pot=player.group.total_contribution + player.dictator_action, realcontr=player.group.total_contribution, ) @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: if player.round_number == 1: return 50 else: return 45 @staticmethod def before_next_page(player, timeout_happened): choice_of_communication(player.group) if timeout_happened: player.t_out_choice = True if player.round_number == 1: player.dictator_choice = 2 player.group.communication_choice = player.dictator_choice else: # Choose No Com if previous round No Com, o/w True Info last_round = player.round_number - 1 last_player = player.in_round(last_round) player.dictator_choice = last_player.dictator_choice player.group.communication_choice = player.dictator_choice if last_player.dictator_choice == 3: player.dictator_choice = 2 player.group.communication_choice = player.dictator_choice # Calculate whether player should be excluded from game player.timeout_count += 1 else: player.timeout_count = 0 exclude_inactive(player) # @staticmethod # def app_after_this_page(player, upcoming_apps): # if player.participant.is_dropout: # return upcoming_apps[0] class DictatorCommunication(Page): @staticmethod def is_displayed(player): return player.participant.type == "Dictator" and player.session.treat == 3 form_model = 'player' form_fields = ['communicated_action'] @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: if player.round_number == 1: return 50 else: return 40 # When the total pot of contributions is smaller than 10, # then giving 10 would imply a negative avg contr rate. # This obviously doesn't make much sense... thus, we can define the max contribution as min of {potsize,10}. # Now let's think about the maximum embezzlement. The final communicated amount must be equal to the real pot. # The maximum amount that can be contributed is (# of type A)*10. Thus, the maximum embezzlement is this - real pot # However, the implied contribution rate by Type A's given the realpot cannot be higher than (# of type A)*10. # Wenn ich restricten müsse, eine höher contribution zu kreierien:max(-player.group.total_contribution, int(realpot)-10*(C.PLAYERS_PER_GROUP-1)) @staticmethod def vars_for_template(player): realpot = player.group.total_contribution + player.dictator_action return dict( max_contr=min(int(realpot), 10), max_embezzle=int(realpot)-10*(C.PLAYERS_PER_GROUP-1), realcontr=int(player.group.total_contribution), realaction=int(player.dictator_action) ) @staticmethod def js_vars(player: Player): return dict(real_pot=player.group.total_contribution + player.dictator_action, realcontr=player.group.total_contribution, ) @staticmethod def before_next_page(player, timeout_happened): if timeout_happened: player.t_out_comm = True # If the dictator doesn't decide what to communicate, we will communicate the truth player.communicated_action = player.dictator_action # Calculate whether player should be excluded from game player.timeout_count += 1 else: player.timeout_count = 0 exclude_inactive(player) # @staticmethod # def app_after_this_page(player, upcoming_apps): # if player.participant.is_dropout: # return upcoming_apps[0] # ------------------------ RESULTS PAGES -------------------------- # class ResultsWaitPage(WaitPage): body_text = "Please wait for all participants to make their decision." @staticmethod def after_all_players_arrive(group: Group): set_payoffs(group) communicated_levels(group) last_round_determination(group) final_payoff(group) class ResultsCitizenUnverified(Page): @staticmethod def is_displayed(player): return player.participant.type == "Citizen" and (player.session.treat == 2 or player.group.communication_choice == 1) @staticmethod def vars_for_template(player): return dict( private_acc=int(cu(10)-player.contribution), payoff=format(Decimal(player.float_payoff), '.2f') ) @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 30 class ResultsCitizenTransparency(Page): @staticmethod def is_displayed(player): return player.participant.type == "Citizen" and (player.session.treat == 1 or player.group.communication_choice == 2) @staticmethod def vars_for_template(player): return dict( abs_embezz=int(abs(player.group.embezzlement)), total_contribution=int(player.group.total_contribution), group_embezzlement=int(player.group.embezzlement), private_acc=int(cu(10)-player.contribution), payoff=format(Decimal(player.float_payoff), '.2f') ) @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 30 class ResultsCitizenChosen(Page): @staticmethod def is_displayed(player): return (player.participant.type == "Citizen") and (player.session.treat == 3 or player.group.communication_choice == 3) @staticmethod def vars_for_template(player): return dict( abs_embezz=int(abs(player.group.embezzlement)), abs_comm=int(abs(player.group.comm_action)), private_acc=int(cu(10)-player.contribution), payoff=format(Decimal(player.float_payoff), '.2f'), comm_contribution=int(player.group.comm_contribution) ) @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 30 class ResultsDictator(Page): @staticmethod def is_displayed(player): return player.participant.type == "Dictator" @staticmethod def vars_for_template(player): return dict( abs_embezz=int(abs(player.group.embezzlement)), total_contribution=int(player.group.total_contribution), group_embezzlement=int(player.group.embezzlement), private_acc=int(cu(10)-player.dictator_action), payoff=format(Decimal(player.float_payoff), '.2f') ) @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 30 class ProceedPage(Page): @staticmethod def is_displayed(player): return player.round_number > C.ROUND_END @staticmethod def get_timeout_seconds(player): if player.participant.is_dropout: return 0.1 else: return 15 class PayOff(Page): @staticmethod def is_displayed(player): return player.finished_rounds or player.group.number_of_dropouts >= 3 @staticmethod def vars_for_template(player): # if C.ROUND_END > player.round_number >= C.SELECTED_ROUNDS: # final_payoff(player.group) if player.round_number < C.SELECTED_ROUNDS: template_vars = {} return template_vars # this avoids the below calculations print(player.selected_rounds_game) selected_rounds_game = list(player.selected_rounds_game.split(', ')) print(selected_rounds_game) selected_rounds_beliefs = list(player.selected_rounds_beliefs.split(', ')) print(player.selected_game_income) selected_game_income = list(player.selected_game_income.split(', ')) print(selected_game_income) selected_beliefs_income = list(player.selected_beliefs_income.split(', ')) income_decisions = format(Decimal(player.participant.income_decisions), '.2f') income_game = format(Decimal(player.participant.income_game), '.2f') earning = format(Decimal(player.participant.income_game) * Decimal(C.EXCHANGE_RATE), '.2f') ex_rate = format(10 * Decimal(C.EXCHANGE_RATE), '.2f') template_vars = { 'income_decisions': income_decisions, 'income_game': income_game, 'earning': earning, 'ex_rate': ex_rate } for i in range(len(selected_rounds_game)): template_vars[f'selected_round_game{i + 1}'] = selected_rounds_game[i] template_vars[f'selected_round_belief{i + 1}'] = selected_rounds_beliefs[i] template_vars[f'selected_game_income{i + 1}'] = format(Decimal(selected_game_income[i]), '.2f') template_vars[f'selected_beliefs_income{i + 1}'] = selected_beliefs_income[i] return template_vars @staticmethod def app_after_this_page(player, upcoming_apps): return upcoming_apps[0] # ------------------------------------------------------------------------------------------- # # SEQUENCE # # ------------------------------------------------------------------------------------------- # page_sequence = [ GroupWaitPage, RolePage, CitizenPage, DictatorWaitPage, DictatorPage, DictatorChoice, CitizenBeliefs, DictatorCommunication, ResultsWaitPage, ResultsCitizenTransparency, ResultsCitizenUnverified, ResultsCitizenChosen, ResultsDictator, ProceedPage, PayOff ]