##################################################### ##################################################### # 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 math import random import stb_rdm_constants as _K c = cu doc = '' #################### #### File Paths #### #################### # Constants class C(BaseConstants): NAME_IN_URL = 'stb_rdm_intro' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 NUM_APPS = 1 NUM_SURVEY = 9 NUM_PLAYERS = _K.NUM_PLAYERS NUM_QUOTA = _K.NUM_QUOTA NUM_CAP = _K.NUM_CAP_NOC MAX_PTS = _K.MAX_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) # Intro33 — practice expected payoff slider prac_ep = models.IntegerField(blank=True) # Intro34 — 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) # Intro35 — saw technical details hint for assignment probability payoffs saw_ap_hint = models.BooleanField(initial=False, blank=True) # Intro17 — quiz answers (Q1-3); Intro18 — 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(choices=[1, 25, 124, 150]) # priority Island E; correct: 124 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) # Intro22 — practice round ranking (2 islands: A and B) prac_round_rank1 = models.StringField(blank=True) prac_round_rank2 = models.StringField(blank=True) # Intro24 — exploration: number of times subject clicked Change Ranking / Change Priorities prac23_changed_ranking = models.IntegerField(initial=0) prac23_changed_priority = models.IntegerField(initial=0) # Intro09 — number of times participant pressed "Draw New Payoffs" num_pf_draws = models.IntegerField(initial=0) # Intro13 — number of times participant pressed "Draw New Priority Scores" num_pr_draws = models.IntegerField(initial=0) # Intro38 — quiz: assignment probability beliefs (correct: 100 for all) quiz2_beliefA = models.IntegerField(min=0, max=100) quiz2_beliefB = models.IntegerField(min=0, max=100) quiz2_beliefC = models.IntegerField(min=0, max=100) quiz2_beliefD = models.IntegerField(min=0, max=100) quiz2_beliefE = models.IntegerField(min=0, max=100) quiz2_beliefF = models.IntegerField(min=0, max=100) quiz2_err_ct = models.IntegerField(initial=0) # Intro39 — quiz: assignment process question (correct: 'a') quiz3_q1 = models.StringField(choices=['a', 'b', 'c', 'd']) quiz3_err_ct = models.IntegerField(initial=0) ###### Functions def generate_candidate_graph(payoffs=None, priorities=None, gray_points=False, show_priority_rows=True, show_priority_svg=True, show_points_svg=True, highlight_island=None, annotation=None): """Generate HTML for candidate payoff table and number lines. Parameters ---------- gray_points : bool Renders the Points table row and Points number line at low opacity. show_priority_rows : bool If False, omits Priority Score and % Lower Priority table rows. show_priority_svg : bool If False, omits the Priority Scores number line. show_points_svg : bool If False, omits the Points number line entirely. highlight_island : str or None 'A'–'F': highlights that island column in the table and on the number line, graying all others on the line. 'NONE': grays all dots on the number line (table stays normal, no column highlighted). annotation : dict or None Adds a labeled arrow to the Priority Scores number line. Keys: 'value' (int), 'label' (str, use '\\n' for line breaks). """ if payoffs is None: 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, ] if priorities is None: priorities = [C.NUM_PLAYERS - C.NUM_QUOTA - 1] * 6 bar_colors = ['rgb(21, 96, 130)', 'rgb(233, 113, 50)', 'rgb(25, 107, 36)', 'rgb(15, 158, 213)', 'rgb(160, 43, 147)', 'rgb(209, 209, 209)'] islands = ['A', 'B', 'C', 'D', 'E', 'F'] # Identify highlight column index (None if no specific island is highlighted) hl_letter = highlight_island.upper() if highlight_island else None hl_idx = islands.index(hl_letter) if (hl_letter and hl_letter in islands) else None # Compute pct_lower for each island pct_lower_vals = [] for priority_now in priorities: pct_lower = 100 * (priority_now - 1) / (C.NUM_PLAYERS - 1) if pct_lower != 100: pct_lower_vals.append(f'{pct_lower:3.1f}%') else: pct_lower_vals.append(f'{pct_lower:3.0f}%') # --- Summary Table --- html = '
' html += '' # Row 1: Island names (colored circles) html += '' html += '' for i, island in enumerate(islands): txt_color = '#222' if island == 'F' else 'white' hl_style = ' style="background-color:rgba(15,158,213,0.15)"' if (hl_idx is not None and i == hl_idx) else '' html += (f'') html += '' # Row 2: Payoff (optionally grayed) gray_style = ' style="opacity:0.3"' if gray_points else '' html += f'' html += '' for i, pf in enumerate(payoffs): hl_cell = ' style="background-color:rgba(15,158,213,0.15)"' if (hl_idx is not None and i == hl_idx) else '' html += f'' html += '' if show_priority_rows: # Row 3: Priority Score html += '' html += '' for i, pr in enumerate(priorities): html += f'' html += '' # Row 4: % Lower Priority html += '' html += '' for i, pl in enumerate(pct_lower_vals): html += f'' html += '' html += '
Island' f'{island}
Points{pf:.0f}
Priority Score{pr}
% Lower Priority{pl}
' html += '
' # --- SVG Number Lines --- svg_width = 600 pad = 50 line_width = svg_width - 2 * pad spacing = 20 r = 9 def make_svg(label, values, v_min, v_max, ann=None, hl=None): items = [] for i, val in enumerate(values): x = pad + (val - v_min) / (v_max - v_min) * line_width items.append({'x': x, 'color': bar_colors[i], 'island': islands[i]}) groups = {} for item in items: key = round(item['x']) groups.setdefault(key, []).append(item) for key, group in groups.items(): n = len(group) group.sort(key=lambda p: p['island']) for j, p in enumerate(group): p['y'] = -(n - 1) / 2 * spacing + j * spacing min_sep = 2 * r + 2 for _ in range(300): moved = False for idx_a in range(len(items)): for idx_b in range(idx_a + 1, len(items)): a, b = items[idx_a], items[idx_b] dx = abs(a['x'] - b['x']) if dx >= min_sep: continue needed_dy = math.sqrt(max(0.0, min_sep ** 2 - dx ** 2)) dy = b['y'] - a['y'] if abs(dy) < needed_dy - 0.01: extra = (needed_dy - abs(dy)) / 2 if dy >= 0: a['y'] -= extra b['y'] += extra else: a['y'] += extra b['y'] -= extra moved = True if not moved: break min_y = min(item['y'] for item in items) offset = (r + 8) - min_y for item in items: item['y'] += offset y_axis_local = int(offset) max_y = max(item['y'] for item in items) svg_h = max(70, int(max_y) + r + 20) if ann and not ann.get('above', False): svg_h += 70 s = f'
' s += f'
{label}
' s += f'' # Main horizontal line (light gray when any highlight is active) line_color = '#ccc' if hl is not None else '#888' s += (f'') # Dark segment from min to highlighted island's position if hl and hl != 'NONE' and hl in islands: hl_i = islands.index(hl) x_hl = pad + (values[hl_i] - v_min) / (v_max - v_min) * line_width s += (f'') # Left tick and label s += (f'') s += (f'{v_min}') s += (f'(Lowest)') # Right tick and label x_max_pos = pad + line_width s += (f'') s += (f'{v_max}') s += (f'(Highest)') for item in items: cx = item['x'] cy = item['y'] color = item['color'] island = item['island'] text_color = '#222' if island == 'F' else 'white' is_hl = (hl and hl != 'NONE' and island == hl) dot_opacity = '' if (hl is None or is_hl) else 'opacity: 0.3;' # Highlight rect behind the highlighted dot if is_hl: s += (f'') s += (f'') s += (f'') s += (f'{island}') s += '' # Annotation arrow and label if ann: ann_color = '#2a9d48' x_to = pad + (ann['value'] - v_min) / (v_max - v_min) * line_width if 'from_value' in ann: # Horizontal rightward arrow: label at from_value, arrowhead at value x_from = pad + (ann['from_value'] - v_min) / (v_max - v_min) * line_width x_arrow_start = pad + (ann.get('arrow_start', ann['from_value']) - v_min) / (v_max - v_min) * line_width arrow_end_val = ann.get('arrow_end', ann['value']) x_arrow_end = pad + (arrow_end_val - v_min) / (v_max - v_min) * line_width above = ann.get('above', False) if above: text_y = y_axis_local - 42 y_arrow = text_y + 7 # midpoint of the two text lines (+0 and +14) else: y_arrow = y_axis_local + 28 text_y = y_axis_local + 44 for li, line in enumerate(ann['label'].split('\n')): s += (f'{line}') s += (f'') s += (f'') else: # Original vertical downward arrow x_ann = x_to tip_y = y_axis_local + 8 base_y = y_axis_local + 45 text_y = y_axis_local + 60 s += (f'') s += (f'') for li, line in enumerate(ann['label'].split('\n')): s += (f'{line}') s += '
' return s # Points number line if show_points_svg: if gray_points: points_svg = make_svg('Points', payoffs, C.MIN_PTS, C.MAX_PTS) html += f'
{points_svg}
' else: html += make_svg('Points', payoffs, C.MIN_PTS, C.MAX_PTS, hl=highlight_island) # Priority Scores number line (optional, with optional annotation) if show_priority_svg: html += make_svg('Priority Scores', priorities, 1, C.NUM_PLAYERS, ann=annotation) return html def generate_prac_graph(show_svgs=True): """Generate HTML for the 2-island practice round payoff table and number lines. Fixed values: Island A = 140 pts, Island B = 200 pts, both priority score = 1. 2 travelers, 1 spot per island. Parameters ---------- show_svgs : bool If False, returns only the summary table (no number lines). """ PRAC_PLAYERS = 2 payoffs = [140, 200] priorities = [1, 1] bar_colors = ['rgb(21, 96, 130)', 'rgb(233, 113, 50)'] islands = ['A', 'B'] # pct_lower: 100*(1-1)/(2-1) = 0.0% pct_lower_vals = ['0.0%', '0.0%'] # --- Summary Table --- html = '
' html += '' html += '' for i, island in enumerate(islands): html += (f'') html += '' html += '' for i, pf in enumerate(payoffs): html += f'' html += '' html += '' for i, pr in enumerate(priorities): html += f'' html += '' html += '' for i, pl in enumerate(pct_lower_vals): html += f'' html += '' html += '
Island' f'{island}
Points{pf:.0f}
Priority Score{pr}
% Lower Priority{pl}
' # --- SVG Number Lines --- svg_width = 600 pad = 50 line_width = svg_width - 2 * pad r = 9 def make_svg(label, values, v_min, v_max): items = [] for i, val in enumerate(values): x = pad + (val - v_min) / (v_max - v_min) * line_width items.append({'x': x, 'color': bar_colors[i], 'island': islands[i], 'y': 0.0}) min_sep = 2 * r + 2 for _ in range(300): moved = False for ia in range(len(items)): for ib in range(ia + 1, len(items)): a, b = items[ia], items[ib] dx = abs(a['x'] - b['x']) if dx >= min_sep: continue needed_dy = math.sqrt(max(0.0, min_sep ** 2 - dx ** 2)) dy = b['y'] - a['y'] if abs(dy) < needed_dy - 0.01: extra = (needed_dy - abs(dy)) / 2 if dy >= 0: a['y'] -= extra; b['y'] += extra else: a['y'] += extra; b['y'] -= extra moved = True if not moved: break min_y = min(item['y'] for item in items) offset = (r + 8) - min_y for item in items: item['y'] += offset y_ax = int(offset) max_y = max(item['y'] for item in items) svg_h = max(70, int(max_y) + r + 20) s = f'
' s += f'
{label}
' s += f'' s += (f'') s += (f'') s += (f'{v_min}') s += (f'(Lowest)') x_r = pad + line_width s += (f'') s += (f'{v_max}') s += (f'(Highest)') for item in items: cx, cy = item['x'], item['y'] s += (f'') s += (f'') s += (f'{item["island"]}') s += '' s += '
' return s if show_svgs: html += make_svg('Points', payoffs, C.MIN_PTS, C.MAX_PTS) html += make_svg('Priority Scores', priorities, 1, PRAC_PLAYERS) return html def draw_priorities(treatment='stb'): """Draw island priority scores for the given treatment. Parameters ---------- treatment : str 'stb' — Single Tie-Breaking. One score drawn uniformly at random from 1 to C.NUM_PLAYERS; the same score applies to all six islands. Returns a list of 6 priority scores in island order [A, B, C, D, E, F]. """ if treatment == 'stb': score = random.randint(1, C.NUM_PLAYERS) return [score] * 6 else: raise ValueError(f'Unknown treatment: {treatment!r}') def draw_payoffs(treatment='random'): """Draw island payoffs for the given treatment. Parameters ---------- treatment : str 'random' — one island drawn at random receives C.MIN_PTS, one receives C.MAX_PTS, and the remaining four receive distinct values drawn without replacement from the interior step list [MIN_PTS+STEP, MIN_PTS+2*STEP, ..., MAX_PTS-STEP]. Returns a list of 6 payoffs in island order [A, B, C, D, E, F]. """ if treatment == 'random': step_list = list(range(C.MIN_PTS + C.PTS_STEP, C.MAX_PTS, C.PTS_STEP)) order = list(range(6)) random.shuffle(order) interior = random.sample(step_list, 4) payoffs = [0] * 6 payoffs[order[0]] = C.MIN_PTS payoffs[order[1]] = C.MAX_PTS for k in range(4): payoffs[order[k + 2]] = interior[k] return payoffs else: raise ValueError(f'Unknown treatment: {treatment!r}') ###### Pages class Consent(Page): pass class Intro01(Page): pass class Intro02(Page): pass class Intro03(Page): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, show_points_svg=False, ) 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): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, ) pts_per_dollar = round(1 / player.session.real_world_currency_per_point) return {'pf_graph': graph, 'pts_per_dollar': pts_per_dollar} class Intro05(Page): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='A', ) return {'pf_graph': graph} class Intro06(Page): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='B', ) return {'pf_graph': graph, 'payoff_b': C.MAX_PTS - 2 * C.PTS_STEP} class Intro07(Page): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, highlight_island='NONE', ) return {'pf_graph': graph} class Intro08(Page): @staticmethod def vars_for_template(player): step_list = list(range(C.MIN_PTS + C.PTS_STEP, C.MAX_PTS, C.PTS_STEP)) pts_step_list = '[' + ', '.join(str(x) for x in step_list) + ']' return {'pts_step_list': pts_step_list} class Intro09(Page): @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 Intro10(Page): @staticmethod def vars_for_template(player): 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)) 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']), } class Intro11(Page): @staticmethod def vars_for_template(player): above_quota = C.NUM_PLAYERS - C.NUM_QUOTA two_quotas_below = C.NUM_PLAYERS - 2 * C.NUM_QUOTA return { 'above_quota': above_quota, 'two_quotas_below': two_quotas_below, 'two_quotas_below_plus_one': two_quotas_below + 1, } class Intro12(Page): @staticmethod def vars_for_template(player): pr = C.NUM_PLAYERS - C.NUM_QUOTA - 1 graph = generate_candidate_graph( gray_points=True, annotation={'value': pr, 'from_value': 75, 'arrow_start': 90, 'arrow_end': pr - 8, 'above': True, 'label': 'Your score at\nall islands'}, ) return {'pf_graph': graph} class Intro13(Page): @staticmethod def live_method(player, data): if data.get('action') == 'draw': player.num_pr_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, ] init_priority = C.NUM_PLAYERS - C.NUM_QUOTA - 1 return {'init_payoffs': init_payoffs, 'init_priority': init_priority} class Intro14(Page): pass class Intro15(Page): form_model = 'player' form_fields = [ 'prac_rank1', 'prac_rank2', 'prac_rank3', 'prac_rank4', 'prac_rank5', 'prac_rank6', 'prac_cap2_rank1', 'prac_cap2_rank2', ] class Intro16(Page): pass class Intro17(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = ['quiz_q1', 'quiz_q2', 'quiz_q3'] @staticmethod def vars_for_template(player): graph = generate_candidate_graph( payoffs=[300, 260, 180, 220, 100, 120], priorities=[124] * 6, ) return {'pf_graph': graph} @staticmethod def error_message(player, values): correct = { 'quiz_q1': '220 points', 'quiz_q2': '300 points', 'quiz_q3': 124, } wrong_qs = [] for n, f in [(1, 'quiz_q1'), (2, 'quiz_q2'), (3, 'quiz_q3')]: if values.get(f) != correct[f]: wrong_qs.append(str(n)) 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 Intro18(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 Intro19(Page): pass class Intro20a(Page): pass class Intro20b(Page): pass class Intro20c(Page): pass class Intro20d(Page): pass class Intro21(Page): pass class Intro22(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() return { 'pf_graph': graph, 'rank_fields': ['prac_round_rank1', 'prac_round_rank2'], } class Intro23(Page): @staticmethod def vars_for_template(player): PRAC_COLORS = { 'A': ('rgb(21, 96, 130)', 'white'), 'B': ('rgb(233, 113, 50)', 'white'), } PAYOFFS = {'A': 140, 'B': 200} 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 always loses the tie at first choice. # If they ranked 2 islands → get their second choice. # If they ranked only 1 island → unassigned (no second choice to fall back to). 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, 'assigned': assigned, 'points': points, 'unassigned': unassigned, 'ranking_rows': ranking_rows, 'pool_islands': pool_islands, 'prac_table': generate_prac_graph(show_svgs=False), } class Intro24(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 Intro25(Page): pass class Intro26(Page): pass class Intro27(Page): @staticmethod def vars_for_template(player): graph = generate_candidate_graph( show_priority_rows=False, show_priority_svg=False, ) pts_per_dollar = round(1 / player.session.real_world_currency_per_point) return {'pf_graph': graph, 'pts_per_dollar': pts_per_dollar} class Intro28(Page): @staticmethod def vars_for_template(player): step_list = list(range(C.MIN_PTS + C.PTS_STEP, C.MAX_PTS, C.PTS_STEP)) pts_step_list = '[' + ', '.join(str(x) for x in step_list) + ']' return {'pts_step_list': pts_step_list} class Intro29(Page): @staticmethod def vars_for_template(player): pr = C.NUM_PLAYERS - C.NUM_QUOTA - 1 graph = generate_candidate_graph( gray_points=True, annotation={'value': pr, 'from_value': 75, 'arrow_start': 90, 'arrow_end': pr - 8, 'above': True, 'label': 'Your score at\nall islands'}, ) return {'pf_graph': graph} class Intro30(Page): @staticmethod def vars_for_template(player): pr = C.NUM_PLAYERS - C.NUM_QUOTA - 1 graph = generate_candidate_graph( gray_points=True, annotation={'value': pr, 'from_value': 75, 'arrow_start': 90, 'arrow_end': pr - 8, 'above': True, 'label': 'Your score at\nall islands'}, ) return {'pf_graph': graph} class Intro31(Page): pass class Intro32(Page): form_model = 'player' form_fields = ['prac_ep'] @staticmethod def vars_for_template(player): return {'slider_min': 0, 'slider_max': C.MAX_PTS} class Intro33(Page): form_model = 'player' form_fields = [ 'prac_beliefA', 'prac_beliefB', 'prac_beliefC', 'prac_beliefD', 'prac_beliefE', 'prac_beliefF', ] class Intro34(Page): form_model = 'player' form_fields = ['saw_ap_hint'] class Intro35(Page): @staticmethod def vars_for_template(player): return {'num_actual_rounds': C.NUM_ROUNDS_PER_TREAT * 2} class Intro36(Page): pass class Intro37(Page): pass class Intro38(Page): preserve_unsubmitted_inputs = True form_model = 'player' form_fields = [ 'quiz2_beliefA', 'quiz2_beliefB', 'quiz2_beliefC', 'quiz2_beliefD', 'quiz2_beliefE', 'quiz2_beliefF', ] @staticmethod def vars_for_template(player): graph = generate_candidate_graph( payoffs=[300, 260, 180, 220, 100, 120], priorities=[150] * 6, ) return {'pf_graph': graph} @staticmethod def error_message(player, values): wrong = [f for f in ['quiz2_beliefA', 'quiz2_beliefB', 'quiz2_beliefC', 'quiz2_beliefD', 'quiz2_beliefE', 'quiz2_beliefF'] if values.get(f) != 100] if wrong: player.quiz2_err_ct += 1 return 'One or more answers are incorrect. Hint: look at your priority scores. Are you guaranteed admission to every island if you report it first?' class Intro39(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, Intro01, Intro02, Intro03, Intro04, Intro05, Intro06, Intro07, Intro08, Intro09, Intro10, Intro11, Intro12, Intro13, Intro14, Intro15, Intro16, Intro17, Intro18, Intro19, Intro20a, Intro20b, Intro20c, Intro20d, Intro21, Intro22, Intro23, Intro24, Intro25, Intro26, Intro27, Intro28, Intro29, Intro30, Intro31, Intro32, Intro33, Intro34, Intro35, Intro36, Intro37, Intro38, Intro39, ]