from otree.api import * import itertools class C(BaseConstants): NAME_IN_URL = 'funi_part2' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 PARTICIPATION_FEE = 5.00 ENDOWMENT = 50 THRESHOLD = 60 MULTIPLIER = 2 CONNECTION_BONUS = 36 DESTRUCTION_PROB = 35 # percent PHASE2_MIN = 13 PHASE4_MIN = 13 PRACTICE_MIN = 8 # minutes BELIEF_TOLERANCE = 5 BELIEF_BONUS = 15 POINTS_PER_BLOCK = 10 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): treatments = itertools.cycle([ ('communication', 'risk'), ('no_communication', 'risk'), ('communication', 'ambiguity'), ('no_communication', 'ambiguity'), ]) for group in subsession.get_groups(): communication, task_type = next(treatments) for player in group.get_players(): player.participant.vars['communication'] = communication player.participant.vars['task_type'] = task_type class Group(BaseGroup): pass class Player(BasePlayer): language = models.StringField( choices=[['de', 'Deutsch'], ['en', 'English']], label='', widget=widgets.RadioSelect, ) # ── Phase 1 comprehension questions ─────────────────────────────────────── # Q1: You=20, partner=25 → total 45 < 60 → NO connection; earnings=(50-20)×2=60 cq1_connection = models.StringField( choices=[['yes', 'Yes / Ja'], ['no', 'No / Nein']], label='', ) cq1_earnings = models.IntegerField(label='', min=0) # Q2: You=30, partner=15 → total 45 < 60; returned before doubling=20; total earnings=40 cq2_returned = models.IntegerField(label='', min=0) cq2_total = models.IntegerField(label='', min=0) # ── Phase 2 comprehension questions ──────────────────────────── cq_phase2_payment = models.IntegerField(label='', min=0) # ── Phase 3 Step 1 & 2 comprehension questions ──────────────────────────── # Q1: risk: prob=35; ambiguity: know=no; both: harder for everyone=no cq3_destruction = models.StringField( choices=[ ['unknown', "Ich weiß es nicht / I don't know"], ['5', '5%'], ['20', '20%'], ['35', '35%'], ['50', '50%'], ['65', '65%'], ['80', '80%'], ['95', '95%'], ], label='', ) # Q2: connection survived → bonus=36; action required=no cq4_bonus = models.IntegerField(label='', min=0) cq4_action = models.StringField( choices=[ ['yes_decision', 'Ich muss eine Entscheidung treffen / I need to make a decision'], ['yes_button', 'Ich muss einen Button klicken / I need to click a button'], ['no', 'Der Bonus wird automatisch gutgeschrieben / The bonus is credited automatically'], ], label='', ) # ── Phase 3 Step 3 & 4 comprehension questions ──────────────────────────── # Q1: know transfer before decision=no; can change after=no cq5_know = models.StringField( choices=[['yes', 'Yes / Ja'], ['no', 'No / Nein']], label='', ) cq5_change = models.StringField( choices=[['yes', 'Yes / Ja'], ['no', 'No / Nein']], label='', ) # Q2: Option A + 30 pts → 1 letter less; 60 s cq6_letters = models.IntegerField(label='', min=0) cq6_seconds = models.IntegerField(label='', min=0) # Q3: Option B + 30 pts → practice=7 min; compensated=6 min; earn=no cq7_practice = models.IntegerField(label='', min=0) cq7_compensated = models.IntegerField(label='', min=0) cq7_earn = models.IntegerField( choices=[ [0, 0], [10, 10], [20, 20], [30, 30], ], label='', ) # Q4: deduct=15; add=30; see before submitting=no cq8_deducted = models.IntegerField(label='', min=0) cq8_effect = models.StringField( choices=[ ['added', 'Sie werden meinen Verdiensten hinzugefügt / They are added to my earnings'], ['deducted', 'Sie werden von meinen Verdiensten abgezogen / They are deducted from my earnings'], ['task', 'Sie erleichtern mir die Aufgabe in Phase 4 / They make my Phase 4 task easier'], ['nothing', 'Sie haben keinen Effekt / They have no effect'], ], label='', ) cq8_simultaneous = models.StringField( choices=[['yes', 'Yes / Ja'], ['no', 'No / Nein']], label='', ) # ── PAGES ───────────────────────────────────────────────────────────────────── class LanguageSelection(Page): form_model = 'player' form_fields = ['language'] @staticmethod def before_next_page(player: Player, timeout_happened): player.participant.language = player.language class Introduction(Page): pass class DataPrivacyInformation(Page): pass class GeneralInstructions(Page): @staticmethod def vars_for_template(player): return dict( participation_fee=f"{C.PARTICIPATION_FEE:.2f}", ) class Phase1Instructions(Page): form_model = 'player' form_fields = [ 'cq1_connection', 'cq1_earnings', 'cq2_returned', 'cq2_total', ] @staticmethod def vars_for_template(player: Player): return dict( communication=player.participant.vars.get('communication', 'no_communication'), ) @staticmethod def error_message(player: Player, values): lang = player.participant.language wrong = [] if values.get('cq1_connection') != 'no': wrong.append('Q1(a)') if values.get('cq1_earnings') != 60: wrong.append('Q1(b)') if values.get('cq2_returned') != 20: wrong.append('Q2(a)') if values.get('cq2_total') != 40: wrong.append('Q2(b)') if wrong: labels = ', '.join(wrong) if lang == 'en': return (f'The following answers are incorrect: {labels}. ' f'Please re-read the instructions above and correct your answers.') return (f'Die folgenden Antworten sind falsch: {labels}. ' f'Bitte lesen Sie die obigen Instruktionen erneut und korrigieren Sie Ihre Antworten.') class Phase2Instructions(Page): form_model = 'player' form_fields = ['cq_phase2_payment'] @staticmethod def error_message(player: Player, values): lang = player.participant.language if values.get('cq_phase2_payment') != 180: if lang == 'en': return 'That answer is incorrect. Please re-read the instructions above and try again.' return 'Diese Antwort ist falsch. Bitte lesen Sie die obigen Instruktionen erneut und versuchen Sie es noch einmal.' class Phase3Instructions_Shock(Page): form_model = 'player' form_fields = [ 'cq3_destruction', 'cq4_bonus', 'cq4_action', ] @staticmethod def vars_for_template(player: Player): return dict(task_type=player.participant.vars.get('task_type', 'risk')) @staticmethod def error_message(player: Player, values): lang = player.participant.language task_type = player.participant.vars.get('task_type', 'risk') wrong = [] if task_type == 'risk': if values.get('cq3_destruction') != '35': wrong.append('Q1(a)') else: if values.get('cq3_destruction') != 'unknown': wrong.append('Q1(a)') if values.get('cq4_bonus') != 36: wrong.append('Q2(a)') if values.get('cq4_action') != 'no': wrong.append('Q2(b)') if wrong: labels = ', '.join(wrong) if lang == 'en': return (f'The following answers are incorrect: {labels}. ' f'Please re-read the instructions above and correct your answers.') return (f'Die folgenden Antworten sind falsch: {labels}. ' f'Bitte lesen Sie die obigen Instruktionen erneut und korrigieren Sie Ihre Antworten.') class Phase3Instructions_Decision(Page): form_model = 'player' form_fields = [ 'cq5_know', 'cq5_change', 'cq6_letters', 'cq6_seconds', 'cq7_practice', 'cq7_compensated', 'cq7_earn', 'cq8_deducted', 'cq8_effect', 'cq8_simultaneous', ] @staticmethod def error_message(player: Player, values): lang = player.participant.language wrong = [] if values.get('cq5_know') != 'no': wrong.append('Q1(a)') if values.get('cq5_change') != 'no': wrong.append('Q1(b)') if values.get('cq6_letters') != 1: wrong.append('Q2(a)') if values.get('cq6_seconds') != 60: wrong.append('Q2(b)') if values.get('cq7_practice') != 7: wrong.append('Q3(a)') if values.get('cq7_compensated') != 6: wrong.append('Q3(b)') if values.get('cq7_earn') != 0: wrong.append('Q3(c)') if values.get('cq8_deducted') != 15: wrong.append('Q4(a)') if values.get('cq8_effect') != 'task': wrong.append('Q4(b)') if values.get('cq8_simultaneous') != 'no': wrong.append('Q4(c)') if wrong: labels = ', '.join(wrong) if lang == 'en': return (f'The following answers are incorrect: {labels}. ' f'Please re-read the instructions above and correct your answers.') return (f'Die folgenden Antworten sind falsch: {labels}. ' f'Bitte lesen Sie die obigen Instruktionen erneut und korrigieren Sie Ihre Antworten.') class InstructionsComplete(Page): pass page_sequence = [ LanguageSelection, Introduction, DataPrivacyInformation, GeneralInstructions, Phase1Instructions, Phase2Instructions, Phase3Instructions_Shock, Phase3Instructions_Decision, InstructionsComplete ]