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 += ('' '' '' '') # Row 1: Island names (colored circles) html += '' html += '' 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'') 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): 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'' html += '' if show_priority_rows: # Row 3: Priority Score html += '' html += '' 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'' html += '' # Row 4: % Lower Priority html += '' html += '' 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'' html += '' html += '
Popular Islands
Island' f'{island}
Points{pf:.0f}
Priority Score{cell_val}
% Lower Priority{cell_val}
' 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'' if treatment == 'aligned' and label == 'Points': x_mid_pop = pad + (_K.MID_PTS - _K.PTS_STEP / 2 - v_min) / (v_max - v_min) * line_width x_right_pop = pad + line_width + r + 2 mid_x_pop = (x_mid_pop + pad + line_width) / 2 pop_h = (y_axis_local + 6) * 2 pop_y = y_axis_local - pop_h / 2 s += (f'') s += (f'Popular Islands') # 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 ann.get('diagonal', False): # Diagonal arrow: text below line at from_value x, arrowhead angles up to value dot x_from_pos = pad + (ann['from_value'] - v_min) / (v_max - v_min) * line_width text_y = y_axis_local + 44 for li, line in enumerate(ann['label'].split('\n')): s += (f'{line}') ax1, ay1 = x_from_pos, text_y - 14 ax2, ay2 = x_to, y_axis_local + r + 2 dx_a, dy_a = ax2 - ax1, ay2 - ay1 length = math.sqrt(dx_a * dx_a + dy_a * dy_a) ux, uy = dx_a / length, dy_a / length px, py = -uy, ux s += (f'') s += (f'') elif '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, _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'' s += (f'') s += (f'') s += (f'{v_min}') s += (f'(Lowest)') x_max_h = pad_h + lw_h s += (f'') s += (f'{v_max}') s += (f'(Highest)') cx_mid = pad_h + lw_h / 2 s += (f'?') s += '
' 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 += '' 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): cell_val = '?' if hide_priorities else pr html += f'' html += '' html += '' for i, pl in enumerate(pct_lower_vals): cell_val = '?' if hide_priorities else pl html += f'' html += '' html += '
Island' f'{island}
Points{pf:.0f}
Priority Score{cell_val}
% Lower Priority{cell_val}
' # --- 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, _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'' s += (f'') s += (f'') s += (f'1') s += (f'(Lowest)') x_max_h = pad_h + lw_h s += (f'') s += (f'{PRAC_PLAYERS}') s += (f'(Highest)') cx_mid = pad_h + lw_h / 2 s += (f'?') s += '
' 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}')