from otree.api import * import random from scenario_text import SCENARIOS import time class C(BaseConstants): NAME_IN_URL = 'time_choice' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3 TOTAL_MINUTES_PART2 = 16 MIN_PUZZLE_MIN = 5 MIN_SLIDER_MIN = 1 # Pilot uses 5 slider rates (from session config) PILOT_RATE_IDS = [1, 2, 3, 4, 5] class Subsession(BaseSubsession): def creating_session(self): for pl in self.get_players(): part = pl.participant # Randomize scenario order once per participant for both main and pilot if 'scenario_order' not in part.vars: order = [1, 2, 3] random.shuffle(order) part.vars['scenario_order'] = order if 'scenario_text' not in part.vars: part.vars['scenario_text'] = SCENARIOS class Group(BaseGroup): pass def is_pilot(player) -> bool: return bool(player.session.config.get('sessionPilot', False)) def slider_rate_for(session, rate_id: int) -> float: # Map rate_id to the correct session config key 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 clamp_default_puzzle_minutes() -> int: max_p = C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN # With total=3 and min slider=1, max puzzle is 2. Default to 2. return max(C.MIN_PUZZLE_MIN, min(10, max_p)) 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 Player(BasePlayer): # Comprehension questions comp_decisions = models.IntegerField( choices=[1, 2, 3, 4, 5], label="Pour combien de scénarios allez-vous devoir prendre une décision ?", ) comp_scenarios = models.IntegerField( choices=[1, 2, 3, 4, 5], label="Combien de scénarios seront mis en oeuvre au final ?", ) comp_priming = models.BooleanField( choices=[[True, "Vrai"], [False, "Faux"]], label="Les résultats dans ce type d’exercices de logique sont souvent liés à la réussite scolaire et aux revenus futurs.", ) comp_reflexion = models.BooleanField( choices=[[True, "Vrai"], [False, "Faux"]], label="Vos décisions doivent être vivement réfléchies car l’une d’entre elles déterminera le déroulement de la Partie 2 de l’expérience.", ) # Comprehension tracking comp_total_mistakes = models.IntegerField(initial=0) comp_attempts = models.IntegerField(initial=0) # Main: one allocation per round puzzle_minutes = models.IntegerField( min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True, ) slider_minutes = models.IntegerField(blank=True) scenario_id = models.IntegerField() scenario_order_str = models.StringField() scenario_label = models.StringField(blank=True) # Pilot: five allocations on one page per scenario (puzzle minutes for each rate) puzz_r1 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True) puzz_r2 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True) puzz_r3 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True) puzz_r4 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True) puzz_r5 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True) # Beliefs (unchanged) belief_puzz1 = models.IntegerField(min=0) belief_puzz2 = models.IntegerField(min=0) belief_puzz3 = models.IntegerField(min=0) timechoice_start = models.FloatField(initial=0) timechoice_time_spent = models.FloatField(initial=0) class Instructions(Page): @staticmethod def is_displayed(player): # Show instructions once for everyone except session1 return (not player.session.config.get('session1', False)) and player.round_number == 1 @staticmethod def vars_for_template(player): return dict( is_pilot=bool(player.session.config.get('sessionPilot', False)), is_session1=bool(player.session.config.get('session1', False)), piece_rate= player.session.config.get('piece_rate'), num_correct = player.participant.vars.get('part1_num_correct'), correct_per_min = player.participant.vars.get('part1_correct_per_min') ) class Instructions2(Page): allow_back_button = True @staticmethod def is_displayed(player): # Show instructions once for everyone except session1 return (not player.session.config.get('session1', False)) and player.round_number == 1 @staticmethod def vars_for_template(player): return dict( is_pilot=bool(player.session.config.get('sessionPilot', False)), is_session1=bool(player.session.config.get('session1', False)), piece_rate= player.session.config.get('piece_rate'), num_correct = player.participant.vars.get('part1_num_correct'), correct_per_min = player.participant.vars.get('part1_correct_per_min') ) class Comprehension(Page): allow_back_button = True form_model = 'player' form_fields = [ 'comp_decisions', 'comp_scenarios', 'comp_priming', 'comp_reflexion', ] @staticmethod def is_displayed(player): return (not player.session.config.get('session1', False)) and player.round_number == 1 @staticmethod def error_message(player: Player, values): correct_decisions = 3 correct_scenarios = 1 correct_priming = True correct_reflexion = True errors = {} mistakes_this_attempt = 0 if values.get('comp_decisions') != correct_decisions: errors['comp_decisions'] = "Réponse incorrecte." mistakes_this_attempt += 1 if values.get('comp_scenarios') != correct_scenarios: errors['comp_scenarios'] = "Réponse incorrecte." mistakes_this_attempt += 1 if values.get('comp_priming') is not correct_priming: errors['comp_priming'] = "Réponse incorrecte." mistakes_this_attempt += 1 if values.get('comp_reflexion') is not correct_reflexion: errors['comp_reflexion'] = "Réponse incorrecte." mistakes_this_attempt += 1 # If wrong: record mistakes + block progress with field errors if errors: player.comp_total_mistakes += mistakes_this_attempt player.comp_attempts += 1 return errors return None class TimeChoice(Page): @staticmethod def get_form_fields(player: Player): if is_pilot(player): return ['puzz_r1', 'puzz_r2', 'puzz_r3', 'puzz_r4', 'puzz_r5'] return ['puzzle_minutes'] form_model = 'player' @staticmethod def vars_for_template(player: Player): total = C.TOTAL_MINUTES_PART2 min_p = C.MIN_PUZZLE_MIN min_s = C.MIN_SLIDER_MIN max_p = total - min_s part = player.participant order = part.vars.get('scenario_order') if order is None: order = [1, 2, 3] random.shuffle(order) part.vars['scenario_order'] = order scenario_id = int(order[player.round_number - 1]) scenario = SCENARIOS[scenario_id] player.scenario_id = scenario_id player.scenario_label = scenario["label"] player.scenario_order_str = "-".join(str(x) for x in order) start_default = clamp_default_puzzle_minutes() # Build slider rates list for pilot display rates = [] if is_pilot(player): for rid in C.PILOT_RATE_IDS: rates.append(dict( rate_id=rid, slider_rate=slider_rate_for(player.session, rid), field_name=f"puzz_r{rid}", start_value=start_default, )) if not player.timechoice_start: player.timechoice_start = time.time() # Main start value see = player.field_maybe_none('puzzle_minutes') start_puzzle_minutes = see if see is not None else start_default # 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(player), total_minutes=total, min_puzzle=min_p, min_slider=min_s, max_puzzle=max_p, scenario_id=scenario_id, scenario_label=scenario["label"], scenario_title=scenario.get("title", f"Scénario {scenario['label']}"), scenario_rules_html=scenario.get("rules_html", ""), scenario_visuel_html=scenario.get("visuel_html", ""), scenario_order=order, round_number=player.round_number, # main start_puzzle_minutes=start_puzzle_minutes, # pilot pilot_rates=rates, your_rank=your_rank, table_rows=table_rows, your_rankNoScores = your_rankNoScores, table_rowsNoScores = table_rowsNoScores, num_correct=player.participant.vars.get('part1_num_correct'), correct_per_min=player.participant.vars.get('part1_correct_per_min'), piece_rate=player.session.config.get('piece_rate') ) @staticmethod def error_message(player: Player, values): total = C.TOTAL_MINUTES_PART2 min_slider = C.MIN_SLIDER_MIN min_puzzle = C.MIN_PUZZLE_MIN max_puzzle = total - min_slider if is_pilot(player): for rid in C.PILOT_RATE_IDS: v = values.get(f"puzz_r{rid}") if v is None: continue if v < min_puzzle or v > max_puzzle: return f"Veuillez choisir une valeur entre {min_puzzle} et {max_puzzle}." return v = values.get("puzzle_minutes") if v is not None and (v < min_puzzle or v > max_puzzle): return f"Veuillez choisir une valeur entre {min_puzzle} et {max_puzzle}." @staticmethod def before_next_page(player: Player, timeout_happened): p = player.participant total_min = C.TOTAL_MINUTES_PART2 sid = int(player.scenario_id) if is_pilot(player): # Store 5 allocations for this scenario, keyed by rate for rid in C.PILOT_RATE_IDS: puzzle_min = int(getattr(player, f"puzz_r{rid}")) slider_min = total_min - puzzle_min # Store SECONDS p.vars[f'scenario_{sid}_puzzle_minutes_r{rid}'] = puzzle_min * 60 p.vars[f'scenario_{sid}_slider_minutes_r{rid}'] = slider_min * 60 # For convenience/backward compatibility, also store baseline (r1) into old keys p.vars[f'scenario_{sid}_puzzle_minutes'] = int(getattr(player, "puzz_r1")) * 60 p.vars[f'scenario_{sid}_slider_minutes'] = (total_min - int(getattr(player, "puzz_r1"))) * 60 else: puzzle_min = int(player.puzzle_minutes) slider_min = total_min - puzzle_min player.slider_minutes = slider_min p.vars[f'scenario_{sid}_puzzle_minutes'] = puzzle_min * 60 p.vars[f'scenario_{sid}_slider_minutes'] = slider_min * 60 p.vars['scenario_order_str'] = player.scenario_order_str player.timechoice_time_spent = time.time() - player.timechoice_start class Beliefs(Page): form_model = 'player' form_fields = ['belief_puzz1', 'belief_puzz2', 'belief_puzz3'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): p = player.participant def minutes_for(sid: int): puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes') slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes') title = SCENARIOS[sid]["title"] rappels_html = SCENARIOS[sid].get("rappels_html", "") 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 return puzzle_minutes, slider_minutes, title, rappels_html pz1, sl1, title1, rappels_html1 = minutes_for(1) pz2, sl2, title2, rappels_html2 = minutes_for(2) pz3, sl3, title3, rappels_html3 = minutes_for(3) return dict( label1=title1, label2=title2, label3=title3, # reminders pop-up (you called it rappels in template, but in SCENARIOS it's rules_html) rappels1_html=rappels_html1, rappels2_html=rappels_html2, rappels3_html=rappels_html3, puzzle_minutes_1=pz1, slider_minutes_1=sl1, puzzle_minutes_2=pz2, slider_minutes_2=sl2, puzzle_minutes_3=pz3, slider_minutes_3=sl3, is_pilot=bool(player.session.config.get('sessionPilot', False)), slider_rate=player.session.config.get('sliders_rate'), num_correct=player.participant.vars.get('part1_num_correct'), correct_per_min=player.participant.vars.get('part1_correct_per_min'), ) class Transition(Page): @staticmethod def is_displayed(player: Player):# show only for pilot participants, and only once (after round 3) return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): return dict( is_pilot=bool(player.session.config.get('sessionPilot', False)), ) page_sequence = [Instructions, Instructions2, Comprehension,TimeChoice, Beliefs, Transition]