from otree.api import * import random from time import time from itertools import product c = Currency doc = """ This app contains the interactive components of the experiment. """ class Constants(BaseConstants): name_in_url = 'csr_task' players_per_group = 2 num_rounds = 10 # Initial amount allocated to each player endowment = 1000 fixed_y_amount = 400 max_csr_amount = 600 exchange_rate = 0.01 donation_percentage = "10%" conversion = 300.0 bonus_Percentage = 0.1 min_payoff_to_pay = 300 min_to_donate = 5 max_seconds_on_page = 60 * 1 max_seconds_waiting_for_match = 60 * 2 peq_fields = [ "gender", "age", "ethnicity", "professional_experience", "financial_experience", "interest_sustain", "reason_for_allocation", "attention_check" ] treatments = ['A', 'B', 'C', 'D'] roles = ['DM', 'COO'] class Subsession(BaseSubsession): pass class Group(BaseGroup): treatment = models.StringField() has_dropout = models.BooleanField(initial=False) csr_amount = models.FloatField(min=0, max=Constants.max_csr_amount, doc="""Amount COO sends for CSR Project""") profit_share = models.IntegerField() profit_share_sent = models.FloatField(min=0, doc="""What DM sends to COO""") profit = models.FloatField() class Player(BasePlayer): unmatched = models.BooleanField(initial=False) timeout_happened = models.BooleanField(initial=False) dropout = models.BooleanField(initial=False) save_csr = models.FloatField() dollar_payout = models.FloatField(initial=0) csr_donate = models.FloatField(initial=0) donation_amount = models.FloatField(initial=0) # FUNCTIONS def creating_session(subsession: Subsession): session = subsession.session if subsession.round_number == 1: session.past_pairs = dict() for p in subsession.get_players(): participant = p.participant participant.unmatched = False participant.dropout = False session.past_pairs[subsession.round_number] = list() def group_by_arrival_time_method(subsession: Subsession, waiting_players): const = Constants players_by_treatments = {t: [p for p in waiting_players if p.participant.treatment == t] for t in const.treatments} match = set_match(subsession, players_by_treatments, const) if match: return match for p in waiting_players: if waited_too_long(p): set_unmatched(p) return [p] # Set matches different from previous round. If no match is available, # exclude players who waited for too long def set_match(subsession: Subsession, players_by_treatments, const: Constants): session = subsession.session curr_round = subsession.round_number for treat_group in players_by_treatments.values(): players_by_role = {role: [p for p in treat_group if p.participant.role == role] for role in const.roles} pairs = product(*players_by_role.values()) for pair in pairs: pair_ids = set(p.id_in_subsession for p in pair) if curr_round == 1 or pair_ids not in session.past_pairs[curr_round - 1]: session.past_pairs[curr_round].append(pair_ids) return pair def waited_too_long(player: Player): arrival_time = player.participant.vars.setdefault('arrival_time', time()) return time() - arrival_time >= Constants.max_seconds_waiting_for_match def set_unmatched(player: Player): player.unmatched = True player.participant.unmatched = True def set_group_profit(group: Group): if group.has_dropout: for p in group.get_players(): if not p.dropout: set_unmatched(p) return group.profit = round(Constants.fixed_y_amount * random.uniform(0.5, 1.5), 0) def set_payoffs(group: Group): ps = group.get_players() const = Constants if group.has_dropout: for p in ps: if not p.dropout: set_unmatched(p) return coo = [p for p in ps if p.participant.role == const.roles[1]][0] dm = [p for p in ps if p.participant.role == const.roles[0]][0] coo.payoff = group.profit_share_sent + (group.csr_amount * const.bonus_Percentage) dm.payoff = const.endowment - const.fixed_y_amount - group.csr_amount def set_payout_and_donation(player: Player): if player.donation_amount == 0: const = Constants payoff_to_pay = const.min_payoff_to_pay to_donate = const.min_to_donate participant = player.participant players_matched = [p for p in player.in_all_rounds()] if players_matched: payoff_to_pay = random.choice(players_matched).payoff to_donate = random.choice(players_matched).save_csr participant.payoff = payoff_to_pay player.dollar_payout = round(float(payoff_to_pay) / Constants.conversion, 2) player.csr_donate = to_donate player.donation_amount = round(float(to_donate) / Constants.conversion, 2) def profit_share_sent_error_message(group: Group, value): profit = group.profit if value > profit: return f"The maximum you can send is {profit}" def sent_back_amount_max(player: Player): return player.group.sent_amount # PAGES class TaskPage(Page): timeout_seconds = Constants.max_seconds_on_page @staticmethod def before_next_page(player: Player, timeout_happened): if timeout_happened: participant = player.participant player.timeout_happened = True player.dropout = True participant.dropout = True player.group.has_dropout = True @staticmethod def app_after_this_page(player: Player, upcoming_apps): if player.participant.dropout: return upcoming_apps[-1] class TaskWaitPage(WaitPage): title_text = "Please Wait" body_text = "Kindly stay on this tab and do not navigate to another tab/screen." @staticmethod def app_after_this_page(player: Player, upcoming_apps): if player.participant.unmatched: return upcoming_apps[-1] class GroupingWaitPage(TaskWaitPage): group_by_arrival_time = True body_text = "Waiting to be matched with another participant. Please stay on this browser tab." @staticmethod def after_all_players_arrive(group: Group): num_ps = len(group.get_players()) if num_ps > 1: group.treatment = group.get_player_by_id(1).participant.treatment class COOChoice(TaskPage): form_model = 'group' @staticmethod def get_form_fields(player: Player): fields = [] if player.participant.treatment in ('A', 'D'): fields.append('csr_amount') return fields @staticmethod def is_displayed(player: Player): participant = player.participant return participant.role == "COO" class SendBackWaitPage(TaskWaitPage): @staticmethod def after_all_players_arrive(group: Group): set_group_profit(group) class DMChoice(TaskPage): timeout_seconds = Constants.max_seconds_on_page form_model = 'group' @staticmethod def get_form_fields(player: Player): fields = ['profit_share_sent'] if player.participant.treatment in ('B', 'C'): fields.insert(0, 'csr_amount') return fields @staticmethod def is_displayed(player: Player): participant = player.participant return participant.role == "DM" @staticmethod def vars_for_template(player: Player): profit_share = player.group.profit return dict( profit_share=profit_share, prompt=f'Please an amount from 0 to {profit_share}:' ) class ResultsWaitPage(TaskWaitPage): @staticmethod def after_all_players_arrive(group: Group): set_payoffs(group) body_text = "Please wait on this tab as the results of this round are finalized." class Results(TaskPage): timeout_seconds = Constants.max_seconds_on_page @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group player.save_csr = group.csr_amount participant = player.participant super(Results, Results).before_next_page(player, timeout_happened) if timeout_happened: return if player.round_number < Constants.num_rounds: participant.arrival_time = time() else: set_payout_and_donation(player) class FinalPayout(Page): @staticmethod def is_displayed(player: Player): return player.round_number == Constants.num_rounds @staticmethod def vars_for_template(player: Player): set_payout_and_donation(player) return dict() page_sequence = [GroupingWaitPage, COOChoice, SendBackWaitPage, DMChoice, ResultsWaitPage, Results, FinalPayout]