#####################################################
#####################################################
# 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 += '
Island
'
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'
'
f'{island}
')
html += '
'
# Row 2: Payoff (optionally grayed)
gray_style = ' style="opacity:0.3"' if gray_points else ''
html += f'
'
html += '
Points
'
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'
{pf:.0f}
'
html += '
'
if show_priority_rows:
# Row 3: Priority Score
html += '
'
html += '
Priority Score
'
for i, pr in enumerate(priorities):
html += f'
{pr}
'
html += '
'
# Row 4: % Lower Priority
html += '
'
html += '
% Lower Priority
'
for i, pl in enumerate(pct_lower_vals):
html += f'
{pl}
'
html += '
'
html += '
'
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'
'
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 += '
Island
'
for i, island in enumerate(islands):
html += (f'
'
f'{island}
')
html += '
'
html += '
Points
'
for i, pf in enumerate(payoffs):
html += f'
{pf:.0f}
'
html += '
'
html += '
Priority Score
'
for i, pr in enumerate(priorities):
html += f'
{pr}
'
html += '
'
html += '
% Lower Priority
'
for i, pl in enumerate(pct_lower_vals):
html += f'
{pl}
'
html += '
'
html += '
'
# --- 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'
'
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,
]