import math import random import itertools from otree.api import * import timer_utils doc = """ WS give-or-take dictator game with 3 balanced profiles. """ # --- DATASETS --- PROFILES_MAN = [ {'id': '628b9.....', 'gender': 'Man', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, {'id': '6043d.....', 'gender': 'Man', 'qob': 'Jan-Mar', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'Blue03', 'background': 'Pic_1.jpeg'}, {'id': '66200.....', 'gender': 'Man', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'Gray01', 'background': 'Pic_1.jpeg'}, {'id': '5c846.....', 'gender': 'Man', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '5ebd3.....', 'gender': 'Man', 'qob': 'Apr-Jun', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '5c1d2.....', 'gender': 'Man', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'ShortHairShortWaved', 'clothingType': 'ShirtCrewNeck', 'clothingColor': 'Blue03', 'background': 'Pic_1.jpeg'}, ] PROFILES_CIS_WOMAN = [ {'id': '69bab.....', 'gender': 'Woman', 'qob': 'Apr-Jun', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '69bac.....', 'gender': 'Woman', 'qob': 'Apr-Jun', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '69bac.....', 'gender': 'Woman', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Gray01', 'background': 'Pic_1.jpeg'}, {'id': '63469.....', 'gender': 'Woman', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, {'id': '5e830.....', 'gender': 'Woman', 'qob': 'Apr-Jun', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '67043.....', 'gender': 'Woman', 'qob': 'Jan-Mar', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Gray01', 'background': 'Pic_1.jpeg'}, {'id': '64415.....', 'gender': 'Woman', 'qob': 'Jan-Mar', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, {'id': '67d44.....', 'gender': 'Woman', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Blue03', 'background': 'Pic_1.jpeg'}, {'id': '60f56.....', 'gender': 'Woman', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Blue03', 'background': 'Pic_1.jpeg'}, {'id': '66718.....', 'gender': 'Woman', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, ] PROFILES_TRANS_WOMAN = [ {'id': '5f25f.....', 'gender': 'Transgender', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Blue03', 'background': 'Pic_1.jpeg'}, {'id': '674d8.....', 'gender': 'Transgender', 'qob': 'Apr-Jun', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Black', 'background': 'Pic_1.jpeg'}, {'id': '60a74.....', 'gender': 'Transgender', 'qob': 'Jul-Sep', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, {'id': '6658d.....', 'gender': 'Transgender', 'qob': 'Jan-Mar', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'Gray01', 'background': 'Pic_1.jpeg'}, {'id': '5f66c.....', 'gender': 'Transgender', 'qob': 'Oct-Dec', 'country': 'United Kingdom', 'language': 'English', 'hair': 'LongHairStraight', 'clothingType': 'ShirtScoopNeck', 'clothingColor': 'White', 'background': 'Pic_1.jpeg'}, ] # --- BALANCED TREATMENT SETUP --- GENDERS = ['Transgender', 'Woman', 'Man'] PERMUTATIONS = list(itertools.permutations(GENDERS)) treatment_iterator = itertools.cycle(PERMUTATIONS) class C(BaseConstants): NAME_IN_URL = 'Part3' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3 ENDOWMENT = 150 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): if subsession.round_number == 1: for p in subsession.get_players(): if p.session.config.get('who') == 'dictator': profile_order = next(treatment_iterator) valid_match = False while not valid_match: m = random.choice(PROFILES_MAN) cw = random.choice(PROFILES_CIS_WOMAN) tw = random.choice(PROFILES_TRANS_WOMAN) qobs = {m['qob'], cw['qob'], tw['qob']} colors = {m['clothingColor'], cw['clothingColor'], tw['clothingColor']} if len(qobs) > 2 and len(colors) > 1: valid_match = True profile_dict = {'Man': m, 'Woman': cw, 'Transgender': tw} ordered_profiles = [profile_dict[gender] for gender in profile_order] p.participant.vars['profile_order'] = profile_order p.participant.vars['receiver_profiles'] = ordered_profiles # --- NEW: Create a shuffled deck of questions for this participant --- questions = ['Country_of_birth_receiver', 'qob_receiver', 'top_color_receiver'] random.shuffle(questions) p.participant.vars['available_questions'] = questions class Group(BaseGroup): pass class Player(BasePlayer): total_cost = models.IntegerField() transfer = models.IntegerField( min=-50, max=C.ENDOWMENT, label="How much do you want to give or take?") clearinstructions1 = models.StringField( choices=[ ['strongly_agree', 'Strongly agree'], ['agree', 'Agree'], ['neutral', 'Neither agree nor disagree'], ['disagree', 'Disagree'], ['strongly_disagree', 'Strongly disagree'], ], label='The instructions of this study were clear.', widget=widgets.RadioSelectHorizontal, ) gender_receiver = models.StringField( choices=["Man", "Woman", "Transgender", "Other"], label="What was the selected gender on the matched participant profile?") qob_receiver = models.StringField( choices=["Jan-Mar", "Apr-Jun", "Jul-Sep", "Oct-Dec"], label="What was the quarter of birth of the matched participant?") Country_of_birth_receiver = models.StringField( label="What was the country of birth of the matched participant?", choices=[ "Argentina", "Australia", "Austria", "Belgium", "Brazil", "Canada", "China", "Colombia", "Czech Republic", "Denmark", "Finland", "France", "Germany", "Greece", "Hungary", "India", "Ireland", "Israel", "Italy", "Netherlands", "New Zealand", "Norway", "Poland", "Portugal", "Romania", "Russia", "Saudi Arabia", "Serbia", "Spain", "Sweden", "Switzerland", "Turkey", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Other" ], blank=False ) top_color_receiver = models.StringField( label="What was the color of the top worn by the matched participant's avatar?", choices=[['Blue03', 'Blue'], ['Black', 'Black'], ['White', 'White'], ['Gray01', 'Gray'], ['Red', 'Red']], blank=False ) randomized_round = models.IntegerField() aim = models.StringField( label='What do you think this study was trying to discover?', ) comments = models.StringField( label='Do you have any other comments about this study?', ) Test1 = models.StringField( choices=[['Defi', 'Yes, definitely'], ['Probably', 'Yes, probably'], ['maybe', 'Maybe'], ['Probably Not', 'No, probably not'], ['Definitely Not', 'No, definitely not'], ['IDK', 'I do not know']], label='This is a attention check question. Please select the "Maybe" option', widget=widgets.RadioSelectHorizontal, ) pro_order = models.StringField() profiles_selected = models.StringField() trial_q1 = models.IntegerField( label="What is your initial endowment of tokens for each match?", choices=[[50, '50 tokens'], [100, '100 tokens'], [150, '150 tokens']], widget=widgets.RadioSelect ) trial_q3 = models.IntegerField( label="Among your 3 decisions how many will be randomly selected to determine your and matched participant's bonus payment?", choices=[[1, 'Only 1 decision'], [2, '2 decisions'], [3, 'All 3 decisions']], widget=widgets.RadioSelect ) trial_q2 = models.IntegerField( label="True or False: I will be asked bonus attention question about the matched participant profile or avatar after each decision.", choices=[[1, 'True'], [2, 'False']], widget=widgets.RadioSelect ) trial_q4 = models.IntegerField( label="What is the maximum amount of tokens you can send to or take from the matched participant?", choices=[ [1, 'I can send up to 50, or take up to 100.'], [2, 'I can send up to 100, or take up to 50.'], [3, 'I can send up to 100, or take up to 100.'] ], widget=widgets.RadioSelect ) # --- THE TRACKERS --- trial_q1_fails = models.IntegerField(initial=0) trial_q3_fails = models.IntegerField(initial=0) trial_q2_fails = models.IntegerField(initial=0) trial_q4_fails = models.IntegerField(initial=0) # --- NEW EXP2 TRACKERS --- question_asked = models.StringField() is_correct = models.BooleanField(initial=False) def set_payoff(player: Player): participant = player.participant past_players = player.in_all_rounds() # Randomly select one of the past rounds for the main payoff selected_player = random.choice(past_players) # Check if they got the attention check correct IN THAT SPECIFIC ROUND bonus = 0 if selected_player.is_correct: bonus = 10 # Store variables for the final results page participant.vars['exp2_bonus'] = bonus participant.vars['paying_round'] = selected_player.round_number participant.vars['paying_transfer'] = selected_player.transfer # Calculate final payoff player.payoff = 100 - selected_player.transfer + bonus # PAGES class intro(Page): timer_text = "Time remaining:" @staticmethod def get_timeout_seconds(player): rem = timer_utils.get_remaining_time(player.participant) if rem < timer_utils.WARNING_SECONDS: return rem @staticmethod def is_displayed(player): return (not timer_utils.is_time_up( player.participant) and player.round_number == 1 and player.session.config.get('who') == 'dictator') @staticmethod def before_next_page(player: Player, timeout_happened): player.pro_order = str(player.participant.vars['profile_order']) player.participant.vars['randomized_round'] = random.randint(1, C.NUM_ROUNDS) player.profiles_selected = str(player.participant.vars['receiver_profiles']) if player.session.config.get('who') == 'receiver': player.participant.finished = True class trail(Page): form_model = 'player' form_fields = ['trial_q1', 'trial_q3', 'trial_q4','trial_q2'] @staticmethod def error_message(player: Player, values): errors = dict() if values.get('trial_q1') != 100: player.trial_q1_fails += 1 errors['trial_q1'] = "This answer is wrong." if values.get('trial_q2') != 1: player.trial_q2_fails += 1 errors['trial_q2'] = "This answer is wrong." if values.get('trial_q3') != 1: player.trial_q3_fails += 1 errors['trial_q3'] = "This answer is wrong." if values.get('trial_q4') != 2: player.trial_q4_fails += 1 errors['trial_q4'] = "This answer is wrong." if errors: return errors @staticmethod def is_displayed(player): return (not timer_utils.is_time_up(player.participant) and player.round_number == 1 and player.session.config.get('who') == 'dictator') class d_decision(Page): form_model = "player" form_fields = ["transfer"] timer_text = "Time remaining:" @staticmethod def get_timeout_seconds(player): rem = timer_utils.get_remaining_time(player.participant) if rem < timer_utils.WARNING_SECONDS: return rem @staticmethod def is_displayed(player): return (not timer_utils.is_time_up(player.participant) and player.session.config.get('who') == 'dictator') @staticmethod def vars_for_template(player): return dict(receiver_profile=player.participant.vars['receiver_profiles'][player.round_number - 1]) @staticmethod def js_vars(player): participant = player.participant current_profile = participant.vars['receiver_profiles'][player.round_number - 1] return dict( topc=participant.vars.get('top'), hair1=participant.vars.get('hair'), backgroundim=participant.vars.get('background'), endowment=C.ENDOWMENT, rec_hair=current_profile['hair'], rec_clothingType=current_profile['clothingType'], rec_clothingColor=current_profile['clothingColor'], rec_background=current_profile['background'] ) @staticmethod def before_next_page(player: Player, timeout_happened): # Determine the question for the UPCOMING Exp2 page profile = player.participant.vars['receiver_profiles'][player.round_number - 1] if profile['gender'] == 'Transgender': player.question_asked = 'gender_receiver' else: # --- NEW: Pop a question off the pre-shuffled deck --- available = player.participant.vars['available_questions'] player.question_asked = available.pop() # Save the updated deck back to participant vars player.participant.vars['available_questions'] = available class Exp2(Page): form_model = 'player' timer_text = "Time remaining:" @staticmethod def get_form_fields(player: Player): # Dynamically returns only the question selected in d_decision return [player.question_asked] @staticmethod def get_timeout_seconds(player): rem = timer_utils.get_remaining_time(player.participant) if rem < timer_utils.WARNING_SECONDS: return rem @staticmethod def is_displayed(player): # Shows after every decision return (not timer_utils.is_time_up(player.participant) and player.session.config.get('who') == 'dictator') @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant profile = participant.vars['receiver_profiles'][player.round_number - 1] asked = player.question_asked # Check if the player answered the dynamic question correctly correct = False if asked == 'gender_receiver': correct = (player.gender_receiver == profile['gender']) elif asked == 'Country_of_birth_receiver': correct = (player.Country_of_birth_receiver == profile['country']) elif asked == 'qob_receiver': correct = (player.qob_receiver == profile['qob']) elif asked == 'top_color_receiver': correct = (player.top_color_receiver == profile['clothingColor']) # Record their success for this round player.is_correct = correct class PilotQuestions(Page): form_model = 'player' form_fields = ['aim', 'clearinstructions1', "comments"] timer_text = "Time remaining:" @staticmethod def is_displayed(player): return (not timer_utils.is_time_up(player.participant) and player.session.config.get('trail') == 1 and player.round_number == C.NUM_ROUNDS) @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant participant.finished = True set_payoff(player) class PilotEnd(Page): timer_text = "Time remaining:" @staticmethod def is_displayed(player): return (not timer_utils.is_time_up(player.participant) and player.session.config.get('trail') == 1 and player.round_number == C.NUM_ROUNDS) class Welcome(Page): timer_text = "Time remaining:" def get_timeout_seconds(player): rem = timer_utils.get_remaining_time(player.participant) if rem < timer_utils.WARNING_SECONDS: return rem def is_displayed(player): return ( not timer_utils.is_time_up(player.participant)) and player.round_number == 3 and player.session.config.get( 'who') == 'dictator' and player.session.config.get('trail') != 1 @staticmethod def before_next_page(player: Player, timeout_happened): set_payoff(player) page_sequence = [intro, trail, d_decision, Exp2, PilotQuestions, PilotEnd, Welcome]