from otree.api import * import random import time doc = """ Real-effort slider task: 80 rounds, 1 slider per page. Timing: - Session1: fixed 6 minutes on sliders. - Pilot: time limit read from implemented (scenario, rate_id): participant.vars['implemented_scenario_id'] participant.vars['implemented_rate_id'] participant.vars[f'scenario_{sid}_slider_minutes_r{rid}'] (stored as seconds) - Main: time limit read from implemented scenario allocation: participant.vars['implemented_scenario_id'] participant.vars[f'scenario_{sid}_slider_minutes'] (stored as seconds) - Legacy fallback: participant.vars['slider_total_time_sec'] - Fallback: C.TASK_TOTAL_TIME Payoff: - Session1 + Main: each correct slider earns session.config['sliders_rate']. - Pilot: each correct slider earns the implemented rate for that participant: rate_id=1 -> sliders_rate, rate_id=2 -> sliders_rate2, ... rate_id=5 -> sliders_rate5 - Total stored in participant.vars['part2_sliders_payoff']. """ class C(BaseConstants): NAME_IN_URL = 'slider_task' PLAYERS_PER_GROUP = None NUM_ROUNDS = 80 SLIDER_MIN = 0 SLIDER_MAX = 100 # fallback global time (seconds) TASK_TOTAL_TIME = 0 # per-page time cap (seconds) PAGE_TIME = 7.5 # Session1 fixed time budgets SESSION1_SLIDER_SECONDS = 6 * 60 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): slider_value = models.IntegerField( min=C.SLIDER_MIN, max=C.SLIDER_MAX, blank=True, ) target = models.IntegerField(blank=True) is_correct = models.BooleanField(blank=True) def is_session1(participant) -> bool: return bool(participant.session.config.get('session1', False)) def is_pilot(participant) -> bool: return bool(participant.session.config.get('sessionPilot', False)) def slider_rate_for(participant, rate_id: int) -> float: """ Pilot mapping: 1 -> sliders_rate 2 -> sliders_rate2 ... 5 -> sliders_rate5 """ if rate_id == 1: key = 'sliders_rate' else: key = f'sliders_rate{rate_id}' v = participant.session.config.get(key) if v is None: raise RuntimeError(f"Missing {key} in session config.") return float(v) def get_slider_total_time_sec(participant) -> int: """ Source of truth: - Session1: fixed 6 minutes. - Pilot: implemented (scenario, rate) budget from time_choice: scenario_{sid}_slider_minutes_r{rid} (stored as seconds) - Main: scenario_{sid}_slider_minutes (stored as seconds) - Legacy override: slider_total_time_sec - Fallback: C.TASK_TOTAL_TIME """ # Session1 override if is_session1(participant): return C.SESSION1_SLIDER_SECONDS # Legacy override legacy = participant.vars.get('slider_total_time_sec') if legacy is not None: try: return int(legacy) except Exception: pass sid_raw = participant.vars.get('implemented_scenario_id') if sid_raw is None: return C.TASK_TOTAL_TIME sid = int(sid_raw) # Pilot uses rate-specific key if is_pilot(participant): rid_raw = participant.vars.get('implemented_rate_id') if rid_raw is None: raise RuntimeError("implemented_rate_id not set (pilot session).") rid = int(rid_raw) key = f"scenario_{sid}_slider_minutes_r{rid}" val = participant.vars.get(key) if val is None: raise RuntimeError(f"Missing {key} in participant.vars.") try: return int(val) except Exception: return C.TASK_TOTAL_TIME # Main key = f"scenario_{sid}_slider_minutes" val = participant.vars.get(key) if val is None: return C.TASK_TOTAL_TIME try: return int(val) except Exception: return C.TASK_TOTAL_TIME def get_slider_piece_rate(participant) -> float: """ Main/Session1: session.config['sliders_rate'] Pilot: implemented rate id determines which sliders_rateX applies """ if is_pilot(participant): rid_raw = participant.vars.get('implemented_rate_id') if rid_raw is None: raise RuntimeError("implemented_rate_id not set (pilot session).") return slider_rate_for(participant, int(rid_raw)) return float(participant.session.config.get('sliders_rate', 0)) class SliderTask(Page): form_model = 'player' form_fields = ['slider_value'] @staticmethod def is_displayed(player: Player): p = player.participant total_time = get_slider_total_time_sec(p) start = p.vars.get('slider_start_time') if start is None: return True elapsed = time.time() - start return elapsed < total_time @staticmethod def get_timeout_seconds(player: Player): p = player.participant total_time = get_slider_total_time_sec(p) now = time.time() start = p.vars.get('slider_start_time') if start is None: p.vars['slider_start_time'] = now start = now elapsed = now - start remaining_global = total_time - elapsed if remaining_global <= 0: return 0 return min(C.PAGE_TIME, remaining_global) @staticmethod def vars_for_template(player: Player): p = player.participant # ensure global start exists (overall app timer) now = time.time() start = p.vars.get('slider_start_time') if start is None: p.vars['slider_start_time'] = now start = now total_time = get_slider_total_time_sec(p) elapsed_at_render = int(now - start) raw_target = player.field_maybe_none('target') if raw_target is None: player.target = random.randint(C.SLIDER_MIN, C.SLIDER_MAX) t = player.target else: t = raw_target return dict( target=t, min_value=C.SLIDER_MIN, max_value=C.SLIDER_MAX, total_time=total_time, elapsed_at_render=elapsed_at_render, ) @staticmethod def before_next_page(player: Player, timeout_happened): val = player.field_maybe_none('slider_value') if val is None: player.is_correct = False else: player.is_correct = (val == player.target) class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): participant = player.participant rate = get_slider_piece_rate(participant) correct_count = sum( 1 for r in player.in_all_rounds() if r.field_maybe_none('is_correct') is True ) payoff_sliders = correct_count * rate participant.vars['part2_sliders_payoff'] = payoff_sliders if hasattr(participant, 'part2_sliders_payoff'): setattr(participant, 'part2_sliders_payoff', payoff_sliders) return dict( correct_count=correct_count, sliders_rate=rate, payoff_sliders=payoff_sliders, ) page_sequence = [SliderTask, Results]