##################################################### ##################################################### # README # # Program Name: __init__.py # Purpose: Interface Code ##################################################### # # Author: Andrew Olsen # Date Created: 02.03.2026 # Last Updated: 03.31.2026 # ##################################################### #### Modules from otree.api import * import random import all_constants as _K from shared_intro_lib import generate_candidate_graph, generate_prac_graph, draw_payoffs c = cu doc = '' #################### #### File Paths #### #################### # Constants class C(BaseConstants): NAME_IN_URL = 'mtb_ali_intro' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 NUM_APPS = 1 NUM_SURVEY = 9 NUM_CAP = _K.NUM_CAP_NOC MAX_PTS = _K.MAX_PTS MID_PTS = _K.MID_PTS MIN_PTS = _K.MIN_PTS PTS_STEP = _K.PTS_STEP NUM_CAP_TREAT = _K.NUM_CAP_CAP BDM_PAY = _K.BDM_PAY NUM_ROUNDS_PER_TREAT = _K.NUM_ROUNDS_PER_BLOCK # Subsessions class Subsession(BaseSubsession): pass # Groups class Group(BaseGroup): pass # Player class Player(BasePlayer): # Intro15 — practice ranking interfaces (exploration only, not scored) prac_rank1 = models.StringField(blank=True) prac_rank2 = models.StringField(blank=True) prac_rank3 = models.StringField(blank=True) prac_rank4 = models.StringField(blank=True) prac_rank5 = models.StringField(blank=True) prac_rank6 = models.StringField(blank=True) prac_cap2_rank1 = models.StringField(blank=True) prac_cap2_rank2 = models.StringField(blank=True) # Intro17 — practice expected payoff slider prac_ep = models.IntegerField(blank=True) # Intro17 — practice assignment probability beliefs prac_beliefA = models.IntegerField(blank=True, min=0, max=100) prac_beliefB = models.IntegerField(blank=True, min=0, max=100) prac_beliefC = models.IntegerField(blank=True, min=0, max=100) prac_beliefD = models.IntegerField(blank=True, min=0, max=100) prac_beliefE = models.IntegerField(blank=True, min=0, max=100) prac_beliefF = models.IntegerField(blank=True, min=0, max=100) prac_belief0 = models.IntegerField(blank=True, min=0, max=100) # Intro19 — saw technical details hint for assignment probability payoffs saw_ap_hint = models.BooleanField(initial=False, blank=True) # Intro22 — quiz answers (Q1-3); Intro23 — quiz answers (Q4-5) quiz_q1 = models.StringField(choices=['100 points', '180 points', '220 points', '300 points']) # payoff Island D; correct: '220 points' quiz_q2 = models.StringField(choices=['100 points', '180 points', '220 points', '300 points']) # payoff Island A; correct: '300 points' quiz_q3 = models.IntegerField() # priority Island E; correct: num_players - num_quota - 1 (dynamic via quiz_q3_choices) quiz_q4 = models.StringField(choices=['a', 'b', 'c']) # complete sentence; correct: 'a' quiz_q5_a = models.BooleanField(initial=False, blank=True) # Sent to A; correct: False quiz_q5_b = models.BooleanField(initial=False, blank=True) # Sent to B; correct: True quiz_q5_c = models.BooleanField(initial=False, blank=True) # Sent to C; correct: False quiz_q5_d = models.BooleanField(initial=False, blank=True) # Sent to D; correct: False quiz_q5_e = models.BooleanField(initial=False, blank=True) # Sent to E; correct: False quiz_q5_f = models.BooleanField(initial=False, blank=True) # Sent to F; correct: True quiz_q5_none = models.BooleanField(initial=False, blank=True) # Not sent; correct: True quiz_attempts1 = models.IntegerField(initial=0) quiz_attempts2 = models.IntegerField(initial=0) # Intro30 — practice round ranking (2 islands: A and B) prac_round_rank1 = models.StringField(blank=True) prac_round_rank2 = models.StringField(blank=True) # Intro32 — exploration: number of times subject clicked Change Ranking / Change Priorities prac23_changed_ranking = models.IntegerField(initial=0) prac23_changed_priority = models.IntegerField(initial=0) # Intro08 — number of times participant pressed "Draw New Payoffs" num_pf_draws = models.IntegerField(initial=0) # Intro12 — number of times participant pressed "Draw New Priority Scores" num_pr_draws = models.IntegerField(initial=0) # Intro39 — quiz: assignment probability beliefs, ranking A–F (correct: belief1=100, belief2-6=0, belief0=0) quiz2_belief1 = models.IntegerField(min=0, max=100) quiz2_belief2 = models.IntegerField(min=0, max=100) quiz2_belief3 = models.IntegerField(min=0, max=100) quiz2_belief4 = models.IntegerField(min=0, max=100) quiz2_belief5 = models.IntegerField(min=0, max=100) quiz2_belief6 = models.IntegerField(min=0, max=100) quiz2_belief0 = models.IntegerField(min=0, max=100) quiz2_err_ct = models.IntegerField(initial=0) # Intro40 — quiz: assignment probability beliefs, ranking F–A (correct: belief1=100, belief2-6=0, belief0=0) quiz2b_belief1 = models.IntegerField(min=0, max=100) quiz2b_belief2 = models.IntegerField(min=0, max=100) quiz2b_belief3 = models.IntegerField(min=0, max=100) quiz2b_belief4 = models.IntegerField(min=0, max=100) quiz2b_belief5 = models.IntegerField(min=0, max=100) quiz2b_belief6 = models.IntegerField(min=0, max=100) quiz2b_belief0 = models.IntegerField(min=0, max=100) quiz2b_err_ct = models.IntegerField(initial=0) # Intro41 — quiz: assignment process question (correct: 'a') quiz3_q1 = models.StringField(choices=['a', 'b', 'c', 'd']) quiz3_err_ct = models.IntegerField(initial=0) ###### Functions # generate_candidate_graph, generate_prac_graph, draw_payoffs imported from shared_intro_lib def quiz_q3_choices(player): n = player.session.config['num_players'] q = n // 6 return [1, q, n - q - 1, n] ###### Pages class Consent_Pg(Page): pass class Intro01(Page): pass class Intro02(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] return {'num_other_players': n - 1, 'img_path': f'images/instructions/intro_img_{n}.png', 'num_players': n, 'num_quota': n // 6} class Intro03(Page): allow_back_button = True @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, treatment='aligned', ) pts_per_dollar = round(1 / player.session.real_world_currency_per_point) return {'pf_graph': graph, 'pts_per_dollar': pts_per_dollar} class Intro04(Page): allow_back_button = True @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='A', treatment='aligned', ) return {'pf_graph': graph} class Intro05(Page): allow_back_button = True @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='B', treatment='aligned', ) return {'pf_graph': graph, 'payoff_b': C.MAX_PTS - 2 * C.PTS_STEP} class Intro06(Page): allow_back_button = True @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='NONE', treatment='aligned', ) return {'pf_graph': graph} class Intro07(Page): allow_back_button = True @staticmethod def vars_for_template(player): top_step_list = '[' + ', '.join(str(x) for x in [C.MID_PTS, C.MID_PTS + C.PTS_STEP]) + ']' reg_max = C.MID_PTS - C.PTS_STEP reg_step_list = '[' + ', '.join(str(x) for x in range(C.MIN_PTS + C.PTS_STEP, C.MID_PTS, C.PTS_STEP)) + ']' return {'top_step_list': top_step_list, 'reg_max': reg_max, 'reg_step_list': reg_step_list} class Intro08(Page): allow_back_button = True @staticmethod def live_method(player, data): if data.get('action') == 'draw': player.num_pf_draws += 1 @staticmethod def vars_for_template(player): init_payoffs = [ C.MAX_PTS, C.MAX_PTS - 2 * C.PTS_STEP, C.MIN_PTS + 4 * C.PTS_STEP, C.MAX_PTS - 4 * C.PTS_STEP, C.MIN_PTS, C.MIN_PTS + C.PTS_STEP, ] return {'init_payoffs': init_payoffs} class Intro09(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] pad, line_width = 50, 500 # svg_width=600, pad=50 pf = { 'A': C.MAX_PTS, 'B': C.MAX_PTS - 2 * C.PTS_STEP, 'C': C.MIN_PTS + 4 * C.PTS_STEP, 'D': C.MAX_PTS - 4 * C.PTS_STEP, 'E': C.MIN_PTS, 'F': C.MIN_PTS + C.PTS_STEP, } def cx(p): return int(round(pad + (p - C.MIN_PTS) / (C.MAX_PTS - C.MIN_PTS) * line_width)) pop_rect_x = int(round(pad + (C.MID_PTS - C.PTS_STEP / 2 - C.MIN_PTS) / (C.MAX_PTS - C.MIN_PTS) * line_width)) pop_rect_width = (pad + line_width + 11) - pop_rect_x # right edge = pad+lw+r+2 (r=9) pop_text_x = int(round((pop_rect_x + pad + line_width) / 2)) return { 'payoff_a': pf['A'], 'payoff_b': pf['B'], 'payoff_c': pf['C'], 'payoff_d': pf['D'], 'payoff_e': pf['E'], 'payoff_f': pf['F'], 'cx_a': cx(pf['A']), 'cx_b': cx(pf['B']), 'cx_c': cx(pf['C']), 'cx_d': cx(pf['D']), 'cx_e': cx(pf['E']), 'cx_f': cx(pf['F']), 'pop_rect_x': pop_rect_x, 'pop_rect_width': pop_rect_width, 'pop_text_x': pop_text_x, 'num_players': n, 'num_quota': n // 6, } class Intro10(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] q = n // 6 above_quota = n - q two_quotas_below = n - 2 * q return { 'above_quota': above_quota, 'two_quotas_below': two_quotas_below, 'two_quotas_below_plus_one': two_quotas_below + 1, 'num_players': n, 'num_quota': q, } class Intro11(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] graph = generate_candidate_graph( gray_points=True, priorities=[round(x * n / 150) for x in [55, 133, 27, 12, 140, 79]], annotation={'value': round(79 * n / 150), 'from_value': round(115 * n / 150), 'diagonal': True, 'label': 'Your score at\neach island'}, treatment='aligned', num_players=n, ) return {'pf_graph': graph} class Intro12(Page): allow_back_button = True @staticmethod def live_method(player, data): if data.get('action') == 'draw': player.num_pr_draws += 1 @staticmethod def vars_for_template(player): n = player.session.config['num_players'] init_payoffs = [ C.MAX_PTS, C.MAX_PTS - 2 * C.PTS_STEP, C.MIN_PTS + 4 * C.PTS_STEP, C.MAX_PTS - 4 * C.PTS_STEP, C.MIN_PTS, C.MIN_PTS + C.PTS_STEP, ] init_priorities = [round(x * n / 150) for x in [55, 133, 27, 12, 140, 79]] return {'init_payoffs': init_payoffs, 'init_priorities': init_priorities, 'num_players': n, 'num_quota': n // 6} class Intro13(Page): allow_back_button = True class Intro14(Page): allow_back_button = True class Intro15(Page): allow_back_button = True form_model = 'player' form_fields = [ 'prac_rank1', 'prac_rank2', 'prac_rank3', 'prac_rank4', 'prac_rank5', 'prac_rank6', 'prac_cap2_rank1', 'prac_cap2_rank2', ] class Intro21(Page): allow_back_button = True class Intro22(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = ['quiz_q1', 'quiz_q2', 'quiz_q3'] @staticmethod def vars_for_template(player): n = player.session.config['num_players'] q = n // 6 pr = n - q - 1 prios = [round(x * n / 150) for x in [149, 37, 12, 87, 124, 62]] prios[4] = pr graph = generate_candidate_graph( payoffs=[300, 260, 180, 220, 100, 120], priorities=prios, treatment='aligned', num_players=n, ) return {'pf_graph': graph, 'pr': pr, 'num_players': n, 'num_quota': q} @staticmethod def error_message(player, values): n = player.session.config['num_players'] q = n // 6 correct = { 'quiz_q1': '220 points', 'quiz_q2': '300 points', 'quiz_q3': n - q - 1, } wrong_qs = [] for qnum, f in [(1, 'quiz_q1'), (2, 'quiz_q2'), (3, 'quiz_q3')]: if values.get(f) != correct[f]: wrong_qs.append(str(qnum)) if wrong_qs: player.quiz_attempts1 += 1 if player.quiz_attempts1 < 2: nums = ', '.join(wrong_qs[:-1]) last = wrong_qs[-1] joined = (nums + ' and ' + last) if nums else last verb = 'are' if len(wrong_qs) > 1 else 'is' return f'Question{"s" if len(wrong_qs) > 1 else ""} {joined} {verb} incorrect. Please try again.' else: player.participant.passed_quiz = False else: player.participant.passed_quiz = True @staticmethod def app_after_this_page(player, upcoming_apps): if player.participant.passed_quiz == False: return upcoming_apps[-1] class Intro23(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = [ 'quiz_q4', 'quiz_q5_a', 'quiz_q5_b', 'quiz_q5_c', 'quiz_q5_d', 'quiz_q5_e', 'quiz_q5_f', 'quiz_q5_none', ] @staticmethod def error_message(player, values): correct = { 'quiz_q4': 'a', 'quiz_q5_a': False, 'quiz_q5_b': True, 'quiz_q5_c': False, 'quiz_q5_d': False, 'quiz_q5_e': False, 'quiz_q5_f': True, 'quiz_q5_none': True, } wrong_qs = [] if values.get('quiz_q4') != correct['quiz_q4']: wrong_qs.append('4') if any(values.get(f) != correct[f] for f in ['quiz_q5_a', 'quiz_q5_b', 'quiz_q5_c', 'quiz_q5_d', 'quiz_q5_e', 'quiz_q5_f', 'quiz_q5_none']): wrong_qs.append('5') if wrong_qs: player.quiz_attempts2 += 1 if player.quiz_attempts2 < 2: nums = ', '.join(wrong_qs[:-1]) last = wrong_qs[-1] joined = (nums + ' and ' + last) if nums else last verb = 'are' if len(wrong_qs) > 1 else 'is' return f'Question{"s" if len(wrong_qs) > 1 else ""} {joined} {verb} incorrect. Please try again.' else: player.participant.passed_quiz = False else: player.participant.passed_quiz = True @staticmethod def app_after_this_page(player, upcoming_apps): if player.participant.passed_quiz == False: return upcoming_apps[-1] class Intro24(Page): pass class Intro25(Page): pass class Intro26(Page): pass class Intro27(Page): pass class Intro28(Page): pass class Intro29(Page): pass class Intro30(Page): form_model = 'player' form_fields = ['prac_round_rank1', 'prac_round_rank2'] @staticmethod def error_message(player, values): if not values.get('prac_round_rank1', ''): return {'prac_round_rank1': 'Please rank at least one island.'} @staticmethod def vars_for_template(player): graph = generate_prac_graph(priorities=[1, 2], payoffs=[260, 300]) return { 'pf_graph': graph, 'rank_fields': ['prac_round_rank1', 'prac_round_rank2'], } class Intro31(Page): @staticmethod def vars_for_template(player): PRAC_COLORS = { 'A': ('rgb(21, 96, 130)', 'white'), 'B': ('rgb(233, 113, 50)', 'white'), } PAYOFFS = {'A': 260, 'B': 300} ALL_ISLANDS = ['A', 'B'] rank1 = (player.prac_round_rank1 or '').upper() or 'A' rank2 = (player.prac_round_rank2 or '').upper() # Other traveler: same ranking; if subject ranked only 1, complete with missing island if rank2: other_rank1, other_rank2 = rank1, rank2 else: missing = [x for x in ALL_ISLANDS if x != rank1][0] other_rank1, other_rank2 = rank1, missing # Subject priorities: A=1 (loses tie at A), B=2 (wins tie at B). # Other traveler has complementary priorities: A=2, B=1. SUBJECT_WINS_AT = {'B'} if rank1 in SUBJECT_WINS_AT: # Subject wins the tie at their first choice subject_wins = True assigned = rank1 points = PAYOFFS[assigned] unassigned = False else: # Subject loses the tie at their first choice, falls to rank 2 subject_wins = False if rank2: assigned = rank2 points = PAYOFFS[assigned] unassigned = False else: assigned = '' points = 0 unassigned = True # Always 2 ranking rows; second is empty if subject only ranked 1 island ranking_rows = [{'rank': 1, 'island': rank1, 'bg': PRAC_COLORS[rank1][0], 'txt': PRAC_COLORS[rank1][1]}] if rank2: ranking_rows.append({'rank': 2, 'island': rank2, 'bg': PRAC_COLORS[rank2][0], 'txt': PRAC_COLORS[rank2][1]}) else: ranking_rows.append({'rank': 2, 'island': '', 'bg': '', 'txt': ''}) # Pool contains the unranked island when only 1 was ranked, otherwise empty if unassigned: unranked = [x for x in ALL_ISLANDS if x != rank1][0] pool_islands = [{'island': unranked, 'bg': PRAC_COLORS[unranked][0], 'txt': PRAC_COLORS[unranked][1]}] else: pool_islands = [] return { 'rank1': rank1, 'rank2': rank2, 'other_rank1': other_rank1, 'other_rank2': other_rank2, 'first_choice': rank1, 'subject_wins': subject_wins, 'assigned': assigned, 'points': points, 'unassigned': unassigned, 'ranking_rows': ranking_rows, 'pool_islands': pool_islands, 'prac_table': generate_prac_graph(show_svgs=False, priorities=[1, 2], payoffs=[260, 300]), } class Intro32(Page): form_model = 'player' form_fields = ['prac23_changed_ranking', 'prac23_changed_priority'] @staticmethod def error_message(player, values): cr = values.get('prac23_changed_ranking', 0) cp = values.get('prac23_changed_priority', 0) if not cr or not cp: return 'Please adjust both the island rankings and the priorities at least once before proceeding.' @staticmethod def vars_for_template(player): ALL_ISLANDS = ['A', 'B'] rank1 = (player.prac_round_rank1 or '').upper() or 'A' rank2 = (player.prac_round_rank2 or '').upper() init_ranking = rank1 + rank2 if rank2 else rank1 if rank2: other_rank1, other_rank2 = rank1, rank2 else: missing = [x for x in ALL_ISLANDS if x != rank1][0] other_rank1, other_rank2 = rank1, missing return { 'init_ranking': init_ranking, 'other_rank1': other_rank1, 'other_rank2': other_rank2, } class Intro33(Page): pass class Intro34(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] return {'num_other_players': n - 1, 'img_path': f'images/instructions/intro_img_{n}.png', 'num_players': n, 'num_quota': n // 6} class Intro35(Page): allow_back_button = True @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, treatment='aligned', ) pts_per_dollar = round(1 / player.session.real_world_currency_per_point) top_step_list = '[' + ', '.join(str(x) for x in [C.MID_PTS, C.MID_PTS + C.PTS_STEP]) + ']' reg_max = C.MID_PTS - C.PTS_STEP reg_step_list = '[' + ', '.join(str(x) for x in range(C.MIN_PTS + C.PTS_STEP, C.MID_PTS, C.PTS_STEP)) + ']' return { 'pf_graph': graph, 'pts_per_dollar': pts_per_dollar, 'top_step_list': top_step_list, 'reg_max': reg_max, 'reg_step_list': reg_step_list, } class Intro36(Page): allow_back_button = True @staticmethod def vars_for_template(player): n = player.session.config['num_players'] graph = generate_candidate_graph( gray_points=True, priorities=[round(x * n / 150) for x in [55, 133, 27, 12, 140, 79]], annotation={'value': round(79 * n / 150), 'from_value': round(115 * n / 150), 'diagonal': True, 'label': 'Your score at\neach island'}, treatment='aligned', num_players=n, ) return {'pf_graph': graph} class Intro37(Page): allow_back_button = True @staticmethod def vars_for_template(player): return {'expay_scale_pct': int(_K.EXPAY_SCALE * 100), 'expay_min': 0} class Intro16(Page): allow_back_button = True form_model = 'player' form_fields = ['prac_ep'] @staticmethod def vars_for_template(player): return {'slider_min': 0, 'slider_max': C.MAX_PTS, 'expay_scale_pct': int(_K.EXPAY_SCALE * 100)} class Intro17(Page): allow_back_button = True form_model = 'player' form_fields = [ 'prac_beliefA', 'prac_beliefB', 'prac_beliefC', 'prac_beliefD', 'prac_beliefE', 'prac_beliefF', 'prac_belief0', ] @staticmethod def error_message(player, values): fields = ['prac_beliefA', 'prac_beliefB', 'prac_beliefC', 'prac_beliefD', 'prac_beliefE', 'prac_beliefF', 'prac_belief0'] if all(values.get(f) is None for f in fields): return if any(values.get(f) is None for f in fields): return 'Please enter a probability for every island and for "Not sent to any island".' total = sum(values[f] for f in fields) if total != 100: return f'Your reported probabilities sum to {total}%, but must equal exactly 100%. Please adjust them.' class Intro18(Page): allow_back_button = True form_model = 'player' form_fields = ['saw_ap_hint'] class Intro19(Page): allow_back_button = True @staticmethod def vars_for_template(player): return {'num_actual_rounds': C.NUM_ROUNDS_PER_TREAT * 2} class Intro20(Page): allow_back_button = True class Intro38(Page): allow_back_button = True class Intro39(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = [ 'quiz2_belief1', 'quiz2_belief2', 'quiz2_belief3', 'quiz2_belief4', 'quiz2_belief5', 'quiz2_belief6', 'quiz2_belief0', ] @staticmethod def vars_for_template(player): n = player.session.config['num_players'] graph = generate_candidate_graph( payoffs=[300, 260, 180, 220, 100, 120], priorities=[n, round(32*n/150), round(129*n/150), round(55*n/150), round(82*n/150), n], treatment='aligned', num_players=n, ) island_data = [ ('A', 'rgb(21,96,130)', 'white'), ('B', 'rgb(233,113,50)', 'white'), ('C', 'rgb(25,107,36)', 'white'), ('D', 'rgb(15,158,213)', 'white'), ('E', 'rgb(160,43,147)', 'white'), ('F', 'rgb(209,209,209)', '#222'), ] belief_rows = [ {'rank': i + 1, 'island': letter, 'bg': bg, 'txt': txt, 'field_name': f'quiz2_belief{i + 1}'} for i, (letter, bg, txt) in enumerate(island_data) ] return {'pf_graph': graph, 'belief_rows': belief_rows, 'num_players': n, 'num_quota': n // 6} @staticmethod def error_message(player, values): total = sum(values[f'quiz2_belief{i}'] for i in range(1, 7)) + values['quiz2_belief0'] if total != 100: return f'Your reported probabilities sum to {total}%, but must equal exactly 100%. Please adjust them.' correct = (values.get('quiz2_belief1') == 100 and all(values.get(f'quiz2_belief{i}') == 0 for i in range(2, 7)) and values.get('quiz2_belief0') == 0) if not correct: player.quiz2_err_ct += 1 return 'One or more answers are incorrect. Hint: look at your priority scores. Are you guaranteed admission to your first-ranked island?' class Intro40(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = [ 'quiz2b_belief1', 'quiz2b_belief2', 'quiz2b_belief3', 'quiz2b_belief4', 'quiz2b_belief5', 'quiz2b_belief6', 'quiz2b_belief0', ] @staticmethod def vars_for_template(player): n = player.session.config['num_players'] graph = generate_candidate_graph( payoffs=[300, 260, 180, 220, 100, 120], priorities=[n, round(32*n/150), round(129*n/150), round(55*n/150), round(82*n/150), n], treatment='aligned', num_players=n, ) island_data = [ ('F', 'rgb(209,209,209)', '#222'), ('E', 'rgb(160,43,147)', 'white'), ('D', 'rgb(15,158,213)', 'white'), ('C', 'rgb(25,107,36)', 'white'), ('B', 'rgb(233,113,50)', 'white'), ('A', 'rgb(21,96,130)', 'white'), ] belief_rows = [ {'rank': i + 1, 'island': letter, 'bg': bg, 'txt': txt, 'field_name': f'quiz2b_belief{i + 1}'} for i, (letter, bg, txt) in enumerate(island_data) ] return {'pf_graph': graph, 'belief_rows': belief_rows, 'num_players': n, 'num_quota': n // 6} @staticmethod def error_message(player, values): total = sum(values[f'quiz2b_belief{i}'] for i in range(1, 7)) + values['quiz2b_belief0'] if total != 100: return f'Your reported probabilities sum to {total}%, but must equal exactly 100%. Please adjust them.' correct = (values.get('quiz2b_belief1') == 100 and all(values.get(f'quiz2b_belief{i}') == 0 for i in range(2, 7)) and values.get('quiz2b_belief0') == 0) if not correct: player.quiz2b_err_ct += 1 return 'One or more answers are incorrect. Hint: look at your priority scores. Are you guaranteed admission to your first-ranked island?' class Intro41(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = ['quiz3_q1'] @staticmethod def error_message(player, values): if values.get('quiz3_q1') != 'a': player.quiz3_err_ct += 1 return 'That answer is incorrect. Please try again.' page_sequence = [ Consent_Pg, Intro01, Intro02, Intro03, Intro04, Intro05, Intro06, Intro07, Intro08, Intro09, Intro10, Intro11, Intro12, Intro13, Intro14, Intro15, Intro16, Intro17, Intro18, Intro19, Intro20, Intro21, Intro22, Intro23, Intro24, Intro25, Intro26, Intro27, Intro28, Intro29, Intro30, Intro31, Intro32, Intro33, Intro34, Intro35, Intro36, Intro37, Intro38, Intro39, Intro40, Intro41, ]