from otree.api import * import random from scenario_text import SCENARIOS class C(BaseConstants): NAME_IN_URL = 'scenario_feedback' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 PILOT_RATE_IDS = [1, 2, 3, 4, 5] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): implemented_rate_id = models.IntegerField(blank=True) slider_rate = models.FloatField(blank=True) def _is_pilot(session) -> bool: return bool(session.config.get('sessionPilot', False)) def _slider_rate_for(session, rate_id: int) -> float: if rate_id == 1: key = 'sliders_rate' else: key = f'sliders_rate{rate_id}' v = session.config.get(key) if v is None: raise RuntimeError(f"Missing {key} in session config.") return float(v) def _ensure_implemented_scenario_id(p) -> int: """ If already set upstream, keep it. Otherwise draw uniformly from {1,2,3}. """ sid_raw = p.vars.get('implemented_scenario_id') if sid_raw is None: sid_raw = random.choice([1, 2, 3]) p.vars['implemented_scenario_id'] = sid_raw sid = int(sid_raw) if sid not in SCENARIOS: raise RuntimeError(f"implemented_scenario_id={sid} not found in SCENARIOS.") return sid def _ensure_implemented_rate_id(session, p) -> int | None: """ Pilot only. If already set upstream, keep it. Otherwise draw uniformly from PILOT_RATE_IDS. In main sessions, returns None. """ if not _is_pilot(session): return None rid_raw = p.vars.get('implemented_rate_id') if rid_raw is None: rid_raw = random.choice(C.PILOT_RATE_IDS) p.vars['implemented_rate_id'] = rid_raw rid = int(rid_raw) if rid not in C.PILOT_RATE_IDS: raise RuntimeError(f"implemented_rate_id={rid} is invalid; expected one of {C.PILOT_RATE_IDS}.") return rid def _read_time_allocation_seconds(session, p, sid: int, rid: int | None): """ Returns (puzzle_seconds, slider_seconds). Main: reads scenario_{sid}_puzzle_minutes and scenario_{sid}_slider_minutes. Pilot: reads scenario_{sid}_puzzle_minutes_r{rid} and scenario_{sid}_slider_minutes_r{rid}. """ if _is_pilot(session): if rid is None: raise RuntimeError("Pilot session requires implemented_rate_id but got None.") puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes_r{rid}') slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes_r{rid}') else: puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes') slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes') return puzzle_seconds, slider_seconds def _scenario_id_from_label(label: str) -> int | None: """ Map scenario letter used in WTP (A/B/C) to scenario id used in SCENARIOS (1/2/3). Returns None if label is missing or invalid. """ mapping = {'A': 1, 'B': 2, 'C': 3} return mapping.get(label) def build_ranking_table_force_you_5th(): """ Returns (your_rank, table_rows) such that: - "Vous" is always rank 5 - Opponents are P1..P9 - Cells are placeholders: "P1 performance", "P1 productivité", etc. - If show_opponents is False, opponent performance/productivity are blank """ order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"] rows = [] your_rank = None for rank, label in enumerate(order, start=1): if label == "Vous": your_rank = rank rows.append(dict( rank=rank, time= "Votre décision", participant="Vous", performance="Votre performance", productivity="Votre productivité", )) else: rows.append(dict( rank=rank, participant=label, time= "10 minutes", performance=f"La performance de {label}", productivity=f"La productivité de {label}" )) return your_rank, rows def build_ranking_table_force_you_5th_NoScores(): """ Returns (your_rank, table_rows) such that: - "Vous" is always rank 5 - Opponents are P1..P9 - Cells are placeholders: "P1 performance", "P1 productivité", etc. - If show_opponents is False, opponent performance/productivity are blank """ order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"] rows = [] your_rank = None for rank, label in enumerate(order, start=1): if label == "Vous": your_rank = rank rows.append(dict( rank=rank, participant="Vous", time="Votre décision", performance="Votre performance", productivity="Votre productivité", )) else: rows.append(dict( rank=rank, participant=label, time="10 minutes", performance="", productivity="", )) return your_rank, rows class SelectionFeedback(Page): @staticmethod def vars_for_template(player: Player): p = player.participant session = player.session use_wtp = bool(p.vars.get('use_wtp', False)) is_pilot = _is_pilot(session) # Implemented scenario: # - In main, you may set it upstream via WTP (keep it if present). # - In pilot, WTP is absent, so we draw it here if missing. sid = _ensure_implemented_scenario_id(p) scenario = SCENARIOS[sid] # Implemented slider rate for pilot (single call, no duplication) rid = _ensure_implemented_rate_id(session, p) slider_rate = _slider_rate_for(session, rid) if is_pilot else None slider_rate = round(slider_rate * 7, 2) if is_pilot else None # store for export player.implemented_rate_id = rid player.slider_rate = slider_rate # Read the correct time allocation based on (sid, rid) puzzle_seconds, slider_seconds = _read_time_allocation_seconds(session, p, sid, rid) puzzle_minutes = int(puzzle_seconds) // 60 if puzzle_seconds is not None else None slider_minutes = int(slider_seconds) // 60 if slider_seconds is not None else None # WTP-related info only if WTP was played wtp_pair_label = None wtp_scenario_left = None wtp_scenario_right = None wtp_amount_left = None wtp_amount_right = None has_wtp_amounts = False wtp_left_title = "" wtp_right_title = "" wtp_left_rappels_html = "" wtp_right_rappels_html = "" base_payment_euros = None if use_wtp: wtp_pair_label = p.vars.get('wtp_pay_pair_label') wtp_scenario_left = p.vars.get('wtp_pay_scenario_left') wtp_scenario_right = p.vars.get('wtp_pay_scenario_right') wtp_amount_left_cents = p.vars.get('wtp_pay_amount_left_cents') wtp_amount_right_cents = p.vars.get('wtp_pay_amount_right_cents') wtp_amount_left = (int(wtp_amount_left_cents) / 100) if wtp_amount_left_cents is not None else None wtp_amount_right = (int(wtp_amount_right_cents) / 100) if wtp_amount_right_cents is not None else None has_wtp_amounts = (wtp_amount_left is not None and wtp_amount_right is not None) # Recompose full left/right titles + rappels exactly like in WTPDecision left_id = _scenario_id_from_label(wtp_scenario_left) right_id = _scenario_id_from_label(wtp_scenario_right) if left_id is not None and left_id in SCENARIOS: wtp_left_title = SCENARIOS[left_id].get("title", "") wtp_left_rappels_html = SCENARIOS[left_id].get("rappels_html", "") if right_id is not None and right_id in SCENARIOS: wtp_right_title = SCENARIOS[right_id].get("title", "") wtp_right_rappels_html = SCENARIOS[right_id].get("rappels_html", "") base_payment_cents = p.vars.get('wtp_base_payment_cents') if base_payment_cents is None: base_payment_cents = p.vars.get('guaranteed_payment_cents') base_payment_euros = (int(base_payment_cents) / 100) if base_payment_cents is not None else None # For display your_rank, table_rows = build_ranking_table_force_you_5th() your_rankNoScores, table_rowsNoScores = build_ranking_table_force_you_5th_NoScores() return dict( is_pilot=is_pilot, implemented_scenario_id=sid, implemented_scenario_label=scenario.get("label", str(sid)), implemented_scenario_title=scenario.get("title", f"Scénario {scenario.get('label', sid)}"), implemented_scenario_rappels_html=scenario.get("rappels_html", ""), implemented_rate_id=rid, slider_rate=slider_rate, has_slider_rate=(slider_rate is not None), puzzle_minutes=puzzle_minutes, slider_minutes=slider_minutes, has_time_allocation=(puzzle_minutes is not None and slider_minutes is not None), use_wtp=use_wtp, wtp_pair_label=wtp_pair_label, wtp_scenario_left=wtp_scenario_left, wtp_scenario_right=wtp_scenario_right, wtp_amount_left=wtp_amount_left, wtp_amount_right=wtp_amount_right, has_wtp_amounts=has_wtp_amounts, wtp_left_title=wtp_left_title, wtp_right_title=wtp_right_title, wtp_left_rappels_html=wtp_left_rappels_html, wtp_right_rappels_html=wtp_right_rappels_html, has_wtp_pair_label=(wtp_pair_label is not None and wtp_pair_label != ''), base_payment_euros=base_payment_euros, has_base_payment=(base_payment_euros is not None), your_rank=your_rank, table_rows=table_rows, your_rankNoScores=your_rankNoScores, table_rowsNoScores=table_rowsNoScores, ) page_sequence = [SelectionFeedback]