from otree.api import * import time doc = """ Rank feedback experiment – Puzzle task Part 1 (Raven-type matrices) One matrix per page, global timer determines how many matrices are seen. """ class C(BaseConstants): NAME_IN_URL = 'puzzle_task1' PLAYERS_PER_GROUP = None # Part 1: max 40 matrices NUM_ROUNDS = 40 # Fixed duration for Part 1 (5 minutes) in seconds # (tu avais 60 pour test; mets 5*60 en prod) PART1_TOTAL_TIME = 5*60 # Correct options for matrices 1–40 (values 1–12 = A–L) CORRECT_OPTIONS = [ 7, 1, 8, 6, 5, 8, 5, 1, 9, 6, 3, 1, 7, 12, 4, 11, 12, 1, 1, 6, 2, 6, 5, 3, 12, 7, 1, 1, 8, 1, 9, 5, 1, 1, 9, 12, 4, 5, 4, 11, ] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # Which matrix this round uses (1–40) item_id = models.IntegerField() # Choice A–L encoded as 1–12 answer = models.IntegerField( choices=[ [1, "A"], [2, "B"], [3, "C"], [4, "D"], [5, "E"], [6, "F"], [7, "G"], [8, "H"], [9, "I"], [10, "J"], [11, "K"], [12, "L"] ], widget=widgets.RadioSelect, label="", blank=True, ) # Was this matrix actually shown? viewed = models.BooleanField(initial=False) # Correctness for this matrix (per round) is_correct = models.BooleanField(initial=False) # Time spent on this matrix (seconds, per round) time_spent = models.FloatField(initial=0) # Aggregate if you want to store it (optional) part1_num_correct = models.IntegerField(initial=0) def _ensure_global_start(player: Player) -> float: p = player.participant start = p.vars.get('p1_puzzle_start_time') if start is None: start = time.time() p.vars['p1_puzzle_start_time'] = start return start class Instructions(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): return dict( is_session1=player.session.config.get('session1', False), piece_rate=player.session.config.get('piece_rate', 0), bonus=player.session.config.get('part_participation_bonus', {}).get('part1', 0), ) class Instructions2(Page): allow_back_button = True @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): return dict( is_session1=player.session.config.get('session1', False), piece_rate=player.session.config.get('piece_rate', 0), bonus=player.session.config.get('part_participation_bonus', {}).get('part1', 0), ) class Matrix(Page): form_model = 'player' form_fields = ['answer'] @staticmethod def get_timeout_seconds(player: Player): """ Global timer for Part 1. Remaining time = total - elapsed since first matrix render. """ start = _ensure_global_start(player) elapsed = time.time() - start remaining = C.PART1_TOTAL_TIME - elapsed return max(0, remaining) @staticmethod def is_displayed(player: Player): """ Stop showing pages once the global time is over. """ start = _ensure_global_start(player) elapsed = time.time() - start return elapsed < C.PART1_TOTAL_TIME @staticmethod def vars_for_template(player: Player): p = player.participant start = _ensure_global_start(player) elapsed_at_render = int(time.time() - start) # round -> item_id 1..40 player.item_id = player.round_number # mark as viewed (because we are rendering it now) player.viewed = True # per-matrix start time p.vars['p1_matrix_start_time'] = time.time() q_path = f"rank_feedback/matrices/q/q{player.item_id}.jpg" a_path = f"rank_feedback/matrices/a/a{player.item_id}.jpg" return dict( item_id=player.item_id, q_path=q_path, a_path=a_path, total_time=C.PART1_TOTAL_TIME, elapsed_at_render=elapsed_at_render, ) @staticmethod def before_next_page(player: Player, timeout_happened): p = player.participant # Time spent on this matrix start = p.vars.get('p1_matrix_start_time') if start is not None: player.time_spent = max(0, time.time() - start) else: player.time_spent = 0 # Correctness (if no answer, False) correct_option = C.CORRECT_OPTIONS[player.item_id - 1] answer = player.field_maybe_none('answer') player.is_correct = (answer is not None and answer == correct_option) class Feedback(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): rounds = player.in_all_rounds() viewed_rounds = [r for r in rounds if r.viewed] num_viewed = len(viewed_rounds) num_correct = sum(1 for r in viewed_rounds if r.is_correct) minutes = C.PART1_TOTAL_TIME / 60 correct_per_min = num_correct / minutes if minutes > 0 else 0 correct_per_min_str = f"{correct_per_min:.2f}" # Store performance stats for later p = player.participant p.vars['part1_num_viewed'] = num_viewed p.vars['part1_num_correct'] = num_correct p.vars['part1_correct_per_min'] = correct_per_min # Piece rate from session.config (required) piece_rate = player.session.config.get('piece_rate') if piece_rate is None: raise RuntimeError("Missing session.config['piece_rate'].") # Compute payoff (use oTree Currency) part1_payoff = cu(num_correct * float(piece_rate)) # Store for later apps + set oTree payoff p.vars['part1_payoff'] = part1_payoff player.payoff = part1_payoff # or += if you add other components inside this app return dict( part1_num_viewed=num_viewed, part1_num_correct=num_correct, part1_correct_per_min=correct_per_min_str, part1_piece_rate=piece_rate, part1_payoff=part1_payoff, ) page_sequence = [Instructions, Instructions2, Matrix, Feedback]