import math
import random
import all_constants as _K
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,
treatment='random',
hide_priorities=False,
num_players=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).
Set 'diagonal': True for a diagonal arrow from 'from_value' up to 'value'.
hide_priorities : bool
If True, replaces priority score and % lower priority table cells with '?',
omits island dots from the Priority Scores number line, and places a
centered gray bold '?' below that line. Overrides annotation.
num_players : int or None
Total number of players in the market. Defaults to 150 if None (used
only for static tutorial pages that hide priority data).
"""
if num_players is None:
num_players = 150
num_quota = num_players // _K.NUM_CAP_NOC
if payoffs is None:
payoffs = [
_K.MAX_PTS,
_K.MAX_PTS - 2 * _K.PTS_STEP,
_K.MIN_PTS + 4 * _K.PTS_STEP,
_K.MAX_PTS - 4 * _K.PTS_STEP,
_K.MIN_PTS,
_K.MIN_PTS + _K.PTS_STEP,
]
if priorities is None:
priorities = [num_players - 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) / (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 += '
'
if treatment == 'aligned':
html += ('
'
'
'
'
Popular Islands
'
'
')
# Row 1: Island names (colored circles)
html += '
'
html += '
Island
'
for i, island in enumerate(islands):
txt_color = '#222' if island == 'F' else 'white'
b_sep = '; border-right: 3px solid black' if (treatment == 'aligned' and i == 1) else ''
if hl_idx is not None and i == hl_idx:
hl_style = f' style="background-color:rgba(15,158,213,0.15){b_sep}"'
elif treatment == 'aligned' and i in (0, 1):
hl_style = f' style="background-color:#f0f0f0{b_sep}"'
else:
hl_style = ' style="border-right: 3px solid black"' if b_sep 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):
b_sep = '; border-right: 3px solid black' if (treatment == 'aligned' and i == 1) else ''
if hl_idx is not None and i == hl_idx:
hl_cell = f' style="background-color:rgba(15,158,213,0.15){b_sep}"'
elif treatment == 'aligned' and i in (0, 1):
hl_cell = f' style="background-color:#f0f0f0{b_sep}"'
else:
hl_cell = ' style="border-right: 3px solid black"' if b_sep else ''
html += f'
{pf:.0f}
'
html += '
'
if show_priority_rows:
# Row 3: Priority Score
html += '
'
html += '
Priority Score
'
for i, pr in enumerate(priorities):
b_sep = '; border-right: 3px solid black' if (treatment == 'aligned' and i == 1) else ''
if treatment == 'aligned' and i in (0, 1):
pop_cell = f' style="background-color:#f0f0f0{b_sep}"'
elif b_sep:
pop_cell = ' style="border-right: 3px solid black"'
else:
pop_cell = ''
cell_val = '?' if hide_priorities else pr
html += f'
{cell_val}
'
html += '
'
# Row 4: % Lower Priority
html += '
'
html += '
% Lower Priority
'
for i, pl in enumerate(pct_lower_vals):
b_sep = '; border-right: 3px solid black' if (treatment == 'aligned' and i == 1) else ''
if treatment == 'aligned' and i in (0, 1):
pop_cell = f' style="background-color:#f0f0f0{b_sep}"'
elif b_sep:
pop_cell = ' style="border-right: 3px solid black"'
else:
pop_cell = ''
cell_val = '?' if hide_priorities else pl
html += f'
{cell_val}
'
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) and not ann.get('diagonal', 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, _K.MIN_PTS, _K.MAX_PTS)
html += f'
{points_svg}
'
else:
html += make_svg('Points', payoffs, _K.MIN_PTS, _K.MAX_PTS,
hl=highlight_island)
# Priority Scores number line (optional, with optional annotation)
if show_priority_svg:
if hide_priorities:
v_min, v_max = 1, num_players
svg_width_h = 600
pad_h = 50
lw_h = svg_width_h - 2 * pad_h
y_ax_h = 17
svg_h_h = y_ax_h + 60
s = '
'
s += '
Priority Scores
'
s += f'
'
html += s
else:
html += make_svg('Priority Scores', priorities, 1, num_players, ann=annotation)
return html
def generate_prac_graph(show_svgs=True, priorities=None, payoffs=None, hide_priorities=False):
"""Generate HTML for the 2-island practice round payoff table and number lines.
Default values: Island A = 140 pts, Island B = 200 pts.
2 travelers, 1 spot per island.
Parameters
----------
show_svgs : bool
If False, returns only the summary table (no number lines).
priorities : list of 2 ints
Priority scores for islands [A, B]. Treatment-specific:
STB uses [1, 1]; MTB uses [1, 2].
payoffs : list of 2 ints or None
Payoffs for islands [A, B]. Defaults to [140, 200].
hide_priorities : bool
If True, priority table cells show '?' and the Priority Scores SVG
shows only the number line with a centered gray bold '?' below.
"""
if priorities is None:
raise ValueError('priorities must be provided (e.g. [1, 1] for STB, [1, 2] for MTB)')
if payoffs is None:
payoffs = [140, 200]
PRAC_PLAYERS = 2
bar_colors = ['rgb(21, 96, 130)', 'rgb(233, 113, 50)']
islands = ['A', 'B']
pct_lower_vals = []
for pr in priorities:
pct = 100 * (pr - 1) / (PRAC_PLAYERS - 1)
if pct != 100:
pct_lower_vals.append(f'{pct:3.1f}%')
else:
pct_lower_vals.append(f'{pct:3.0f}%')
# --- 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):
cell_val = '?' if hide_priorities else pr
html += f'
{cell_val}
'
html += '
'
html += '
% Lower Priority
'
for i, pl in enumerate(pct_lower_vals):
cell_val = '?' if hide_priorities else pl
html += f'
{cell_val}
'
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, _K.MIN_PTS, _K.MAX_PTS)
if hide_priorities:
svg_width_h = 600
pad_h = 50
lw_h = svg_width_h - 2 * pad_h
y_ax_h = 17
svg_h_h = y_ax_h + 60
s = '
'
s += '
Priority Scores
'
s += f'
'
html += s
else:
html += make_svg('Priority Scores', priorities, 1, PRAC_PLAYERS)
return html
def draw_payoffs(treatment='random'):
"""Draw island payoffs for the given treatment.
Parameters
----------
treatment : str
'random' — one island drawn at random receives MIN_PTS, one receives
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(_K.MIN_PTS + _K.PTS_STEP, _K.MAX_PTS, _K.PTS_STEP))
order = list(range(6))
random.shuffle(order)
interior = random.sample(step_list, 4)
payoffs = [0] * 6
payoffs[order[0]] = _K.MIN_PTS
payoffs[order[1]] = _K.MAX_PTS
for k in range(4):
payoffs[order[k + 2]] = interior[k]
return payoffs
elif treatment == 'aligned':
top_order = random.sample([0, 1], 2)
top_second = random.choice([_K.MID_PTS, _K.MID_PTS + _K.PTS_STEP])
regular_order = random.sample([2, 3, 4, 5], 4)
reg_interior = list(range(_K.MIN_PTS + _K.PTS_STEP, _K.MID_PTS, _K.PTS_STEP))
reg_three = random.sample(reg_interior, 3)
payoffs = [0] * 6
payoffs[top_order[0]] = _K.MAX_PTS
payoffs[top_order[1]] = top_second
payoffs[regular_order[0]] = _K.MIN_PTS
for k in range(3):
payoffs[regular_order[k + 1]] = reg_three[k]
return payoffs
else:
raise ValueError(f'Unknown treatment: {treatment!r}')