from otree.api import * doc = """ Comprehension Questions for the btpgg """ class C(BaseConstants): NAME_IN_URL = 'cq_btpgg' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 GROUP_SIZE = 5 THRESHOLD = 2 # default; can be overridden via session.config['THRESHOLD'] PUBLIC_GOOD = cu(150) # benefit if threshold met ACTION_ONE = cu(75) # cost if realized action = 1 ACTION_ZERO = cu(30) # cost if realized action = 0 ENDOWMENT = cu(80) # So participants do go negative QUIZ_BONUS_PER = cu(1) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # --- Q1: Threshold met? (positive case) cq_thresh_pos = models.BooleanField(choices=[[True, 'Yes'], [False, 'No']]) cq_thresh_pos_first = models.IntegerField(blank=True) # 1/0 on first try # --- Q2: Threshold NOT met? (negative case) cq_thresh_neg = models.BooleanField(choices=[[True, 'Yes'], [False, 'No']]) cq_thresh_neg_first = models.IntegerField(blank=True) # --- Q3: Set slider to p=0.43 cq_slider_p = models.FloatField(min=0, max=1) cq_slider_first = models.IntegerField(blank=True) # --- Q4: If p(A)=0.23, what is p(B)? cq_prob_b = models.IntegerField(min=0, max=100) cq_prob_b_first = models.IntegerField(blank=True) # --- Q5: if p(A)=0, what is the probability your realized action is action A? cq_realized_a_0 = models.IntegerField(min=0, max=100) cq_realized_a_0_first = models.IntegerField(blank=True) # --- Q6: if p(a)=1, what is the probability your realized action is action A? cq_realized_a_1 = models.IntegerField(min=0, max=100) cq_realized_a_1_first = models.IntegerField(blank=True) # --- Q7: if the realized actions are A A A B B, T/F this means participant 1 played A with probability 1. cq_realized_action_tf = models.BooleanField(choices=[[True, 'Yes'], [False, 'No']]) cq_realized_action_tf_first = models.IntegerField(blank=True) # --- Q8: Suppose a participant selected 0.28. And their realized action was B. The realized actions of the # other members of their group are AAAB. What is the payoff of the participant whose realized action was B? cq_payoff_calc = models.FloatField() cq_payoff_calc_first = models.IntegerField(blank=True) # Total correct cq_first_correct_count = models.IntegerField() cq_bonus = models.FloatField() # FUNCTIONS def get_threshold(obj): # read from settings.py treatment; fallback to C.THRESHOLD return obj.session.config.get('THRESHOLD', C.THRESHOLD) def get_endowment(obj): # Accept plain numbers or cu(...) in settings.py; always wrap here. val = obj.session.config.get('ENDOWMENT', float(C.ENDOWMENT)) return cu(val) # helpers def _set_first_try(flag_field_name: str, is_correct: bool, player): """ Record 1/0 for first-try correctness exactly once. Uses oTree's field_maybe_none() to avoid accessing a null field directly. """ current = player.field_maybe_none(flag_field_name) # safe None check if current is None: setattr(player, flag_field_name, 1 if is_correct else 0) # parameters shown in the questions (tweak if you like) Q_N = 5 # group size Q_Z = 2 # threshold Q_BENEFIT = 150 # public good if provided Q_P_A_TARGET = 0.43 # for slider mapping question Q_P_A_GIVEN = 0.23 # for p(B) question Q_A_0_GIVEN = 0 Q_A_1_GIVEN = 1 def thresh_example_positive(): # ≥ Z A's actions = ['A', 'B', 'A', 'B', 'B'] # K=2 if Z=2 -> provided return actions def thresh_example_negative(): # < Z A's actions = ['B', 'B', 'A', 'B', 'B'] # K=1 if Z=2 -> not provided return actions def actions_to_text(actions): return ", ".join(actions) # PAGES class Welcome(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): return dict( quiz_bonus_cents=C.QUIZ_BONUS_PER ) class CQ_Threshold_Pos(Page): form_model = "player" form_fields = ["cq_thresh_pos"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): actions = thresh_example_positive() k = actions.count('A') will_provide = (k >= Q_Z) return dict( q_n=Q_N, q_z=Q_Z, q_benefit=Q_BENEFIT, actions_text=actions_to_text(actions), k=k, will_provide=will_provide ) @staticmethod def before_next_page(player, timeout_happened): actions = thresh_example_positive() k = actions.count('A') will_provide = (k >= Q_Z) correct = (player.cq_thresh_pos == will_provide) _set_first_try("cq_thresh_pos_first", correct, player) class CQ_Threshold_Neg(Page): form_model = "player" form_fields = ["cq_thresh_neg"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): actions = thresh_example_negative() k = actions.count('A') will_provide = (k >= Q_Z) return dict( q_n=Q_N, q_z=Q_Z, q_benefit=Q_BENEFIT, actions_text=actions_to_text(actions), k=k, will_provide=will_provide ) @staticmethod def before_next_page(player, timeout_happened): actions = thresh_example_negative() k = actions.count('A') will_provide = (k >= Q_Z) correct = (player.cq_thresh_neg == will_provide) _set_first_try("cq_thresh_neg_first", correct, player) class CQ_Slider_Map(Page): form_model = "player" form_fields = ["cq_slider_p"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): return dict(target=int(100*Q_P_A_TARGET)) @staticmethod def before_next_page(player, timeout_happened): # accept small tolerance around 0.43 just incase tol = 0.005 correct = abs(player.cq_slider_p - Q_P_A_TARGET) <= tol _set_first_try("cq_slider_first", correct, player) class CQ_Prob_B(Page): form_model = "player" form_fields = ["cq_prob_b"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): return dict(given_p_a=Q_P_A_GIVEN, expected_b=1 - Q_P_A_GIVEN, for_view_p_a=int(100*Q_P_A_GIVEN),) @staticmethod def before_next_page(player, timeout_happened): tol = 0.001 correct = abs(player.cq_prob_b - int(100*(1 - Q_P_A_GIVEN))) <= tol _set_first_try("cq_prob_b_first", correct, player) class CQ_REALIZED_A_0(Page): form_model = "player" form_fields = ["cq_realized_a_0"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): return dict(given_p_a=Q_A_0_GIVEN, expected_b=0) @staticmethod def before_next_page(player, timeout_happened): correct = abs(player.cq_realized_a_0 - 0) <= 0.001 _set_first_try("cq_realized_a_0_first", correct, player) class CQ_REALIZED_A_1(Page): form_model = "player" form_fields = ["cq_realized_a_1"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player): return dict(given_p_a=Q_A_1_GIVEN, expected_b=1) @staticmethod def before_next_page(player, timeout_happened): correct = abs(player.cq_realized_a_1 - 100) <= 0.001 _set_first_try("cq_realized_a_1_first", correct, player) class CQ_Realized_Action_TF(Page): form_model = "player" form_fields = ["cq_realized_action_tf"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): # Fixed example sequence for this question actions = ['A'] # ['A', 'A', 'A', 'B', 'B'] return dict( tf_actions_text=", ".join(actions), tf_statement="This means participant 1 played action A with probability 100.", ) @staticmethod def before_next_page(player: Player, timeout_happened): # Correct answer is "No" (False): observing outcomes doesn't imply p=1 correct = (player.cq_realized_action_tf is False) _set_first_try("cq_realized_action_tf_first", correct, player) class CQ_Payoff_Calc(Page): form_model = "player" form_fields = ["cq_payoff_calc"] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): # Use live session/treatment params for robustness threshold = get_threshold(player) endowment = get_endowment(player) return dict( given_p="0.28", displayed_p=28, your_realized='B', others_actions="A A A B", players_per_group=C.GROUP_SIZE, threshold=threshold, public_good=C.PUBLIC_GOOD, # cu(...) cost_one=C.ACTION_ONE, # cu(...) cost_zero=C.ACTION_ZERO, # cu(...) endowment=endowment, # cu(...) ) @staticmethod def before_next_page(player: Player, timeout_happened): """ Scenario: - You chose p = 0.28 (irrelevant after realization) - Your realized action = B -> cost = ACTION_ZERO - Others = A A A B -> 3 A's among others - Total A's = 3 (you are B) -> public good provided iff 3 >= threshold - Payoff = endowment + (benefit_if_provided) - cost_of_B """ threshold = get_threshold(player) endowment = get_endowment(player) others_num_A = 3 your_is_A = 0 total_A = others_num_A + your_is_A provided = (total_A >= threshold) benefit = C.PUBLIC_GOOD if provided else cu(0) cost = C.ACTION_ZERO correct_payoff = float(endowment + (benefit - cost)) # convert cu to float correct = abs(player.cq_payoff_calc-correct_payoff) <= 0.001 _set_first_try("cq_payoff_calc_first", correct, player) class CQ_Feedback(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): # Recreate the same prompts the question pages showed: pos_actions = thresh_example_positive() neg_actions = thresh_example_negative() return dict( # Q1 prompt content q_n=Q_N, q_z=Q_Z, pos_actions_text=actions_to_text(pos_actions), pos_k=pos_actions.count('A'), # Q2 prompt content neg_actions_text=actions_to_text(neg_actions), neg_k=neg_actions.count('A'), pos_ans=player.cq_thresh_pos, pos_ok=bool(player.cq_thresh_pos_first), neg_ans=player.cq_thresh_neg, neg_ok=bool(player.cq_thresh_neg_first), slider_ans=player.cq_slider_p, slider_ok=bool(player.cq_slider_first), slider_target=f"{Q_P_A_TARGET:.2f}", slider_target_display= int(100*Q_P_A_TARGET), pb_ans=player.cq_prob_b, pb_ok=bool(player.cq_prob_b_first), given_p_a=f"{Q_P_A_GIVEN:.2f}", given_p_a_display=int(100*Q_P_A_GIVEN), expected_b_display=f"{(1 - Q_P_A_GIVEN):.2f}", expected_b_to_see=int(100*(1-Q_P_A_GIVEN)), # Q5,Q6,Q7,Q8 ra0_given=f"{Q_A_0_GIVEN:.2f}", # p(A)=0 ra0_given_display=int(100*Q_A_0_GIVEN), ra0_ans=player.cq_realized_a_0, ra0_ok=bool(player.cq_realized_a_0_first), ra0_expected_display="0.00", ra1_given=f"{Q_A_1_GIVEN:.2f}", # p(A)=1 ra1_given_display=int(100*Q_A_1_GIVEN), ra1_ans=player.cq_realized_a_1, ra1_ok=bool(player.cq_realized_a_1_first), ra1_expected_display="1.00", tf_ans=player.cq_realized_action_tf, tf_ok=bool(player.cq_realized_action_tf_first), tf_actions_text='A', # "A, A, A, B, B" payoff_ans=player.cq_payoff_calc, payoff_ok=bool(player.cq_payoff_calc_first), pay_given_p="0.28", pay_your_realized='B', pay_others_actions="A A A B", ) @staticmethod def before_next_page(player: Player, timeout_happened): # sum of first try corrects on comp Qs flags = [ player.cq_thresh_pos_first, player.cq_thresh_neg_first, player.cq_slider_first, player.cq_prob_b_first, player.cq_realized_a_0_first, player.cq_realized_a_1_first, player.cq_realized_action_tf_first, player.cq_payoff_calc_first, ] count = sum(1 for v in flags if v == 1) # save for use in PAYMENTS page (separate app) player.participant.vars['cq_first_correct_count'] = count player.participant.vars['cq_bonus'] = count * C.QUIZ_BONUS_PER class MyWaitPageFun(WaitPage): template_name = 'cq_btpgg/MyWaitPageFun.html' wait_for_all_groups = True # waits for *all participants in the session* page_sequence = [Welcome, CQ_Threshold_Pos, CQ_Threshold_Neg, CQ_Slider_Map, CQ_Prob_B, CQ_REALIZED_A_0, CQ_REALIZED_A_1, CQ_Realized_Action_TF, CQ_Payoff_Calc, CQ_Feedback, MyWaitPageFun]