from otree.api import * doc = """ Raven's Progressive Matrices Task. Subjects solve 27 matrices (26 test + 1 example). Payment: 20 cents per correct answer. """ # ── Constants ──────────────────────────────────────────────────────── class C(BaseConstants): NAME_IN_URL = 'raven_matrices' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 # ✏️ Edit this list to use only a subset of matrices. # e.g. MATRIX_INDICES = [1, 2, 3, 10, 15, 20] MATRIX_INDICES = list(range(1, 7)) # default: all 27 list(range(1, 28)) NUM_MATRICES = len(MATRIX_INDICES) CORRECT_ANSWERS = { 1: 1, 2: 1, 3: 5, 4: 2, 5: 5, 6: 8, 7: 4, 8: 2, 9: 4, 10: 3, 11: 3, 12: 2, 13: 5, 14: 5, 15: 4, 16: 4, 17: 2, 18: 5, 19: 6, 20: 6, 21: 4, 22: 5, 23: 3, 24: 2, 25: 6, 26: 8, 27: 2, } PAYMENT_PER_CORRECT = cu(0.20) # 20 cents # ── Models ─────────────────────────────────────────────────────────── class Subsession(BaseSubsession): pass class Group(BaseGroup): pass def make_answer_field(label): return models.IntegerField( label=label, choices=[1, 2, 3, 4, 5, 6, 7, 8], widget=widgets.RadioSelectHorizontal, ) class Player(BasePlayer): # One field per matrix (answer_1 … answer_27) answer_1 = make_answer_field('Your answer for Matrix 1') answer_2 = make_answer_field('Your answer for Matrix 2') answer_3 = make_answer_field('Your answer for Matrix 3') answer_4 = make_answer_field('Your answer for Matrix 4') answer_5 = make_answer_field('Your answer for Matrix 5') answer_6 = make_answer_field('Your answer for Matrix 6') answer_7 = make_answer_field('Your answer for Matrix 7') answer_8 = make_answer_field('Your answer for Matrix 8') answer_9 = make_answer_field('Your answer for Matrix 9') answer_10 = make_answer_field('Your answer for Matrix 10') answer_11 = make_answer_field('Your answer for Matrix 11') answer_12 = make_answer_field('Your answer for Matrix 12') answer_13 = make_answer_field('Your answer for Matrix 13') answer_14 = make_answer_field('Your answer for Matrix 14') answer_15 = make_answer_field('Your answer for Matrix 15') answer_16 = make_answer_field('Your answer for Matrix 16') answer_17 = make_answer_field('Your answer for Matrix 17') answer_18 = make_answer_field('Your answer for Matrix 18') answer_19 = make_answer_field('Your answer for Matrix 19') answer_20 = make_answer_field('Your answer for Matrix 20') answer_21 = make_answer_field('Your answer for Matrix 21') answer_22 = make_answer_field('Your answer for Matrix 22') answer_23 = make_answer_field('Your answer for Matrix 23') answer_24 = make_answer_field('Your answer for Matrix 24') answer_25 = make_answer_field('Your answer for Matrix 25') answer_26 = make_answer_field('Your answer for Matrix 26') answer_27 = make_answer_field('Your answer for Matrix 27') num_correct = models.IntegerField(initial=0) # ── Pages ──────────────────────────────────────────────────────────── class Introduction(Page): """Show example matrix and instructions.""" pass class Matrix(Page): """One page per matrix (1‑27). Uses get_page_sequence dynamic pages.""" form_model = 'player' @staticmethod def get_form_fields(player): idx = player.vars.get('current_matrix', 1) return [f'answer_{idx}'] @staticmethod def vars_for_template(player): idx = player.vars.get('current_matrix', 1) return dict( matrix_number=idx, image_path=f'raven_matrices/{idx}.png', total=C.NUM_MATRICES, ) @staticmethod def before_next_page(player, timeout_happened): idx = player.vars.get('current_matrix', 1) player.vars['current_matrix'] = idx + 1 @staticmethod def app_after_this_page(player, upcoming_apps): # Stay on this page class until all 27 matrices are done pass class Results(Page): @staticmethod def before_next_page(player, timeout_happened): pass @staticmethod def vars_for_template(player): correct = 0 details = [] for i in C.MATRIX_INDICES: given = getattr(player, f'answer_{i}') is_correct = given == C.CORRECT_ANSWERS[i] if is_correct: correct += 1 details.append(dict( matrix=i, given=given, correct_answer=C.CORRECT_ANSWERS[i], is_correct=is_correct, )) player.num_correct = correct player.payoff = correct * C.PAYMENT_PER_CORRECT return dict( num_correct=correct, total=C.NUM_MATRICES, earnings=player.payoff, details=details, ) # Build page_sequence dynamically: Intro + 27 × Matrix + Results # oTree doesn't natively loop a single Page class, so we create 27 # thin subclasses that each know their own matrix index. def _make_matrix_page(matrix_idx): class DynamicMatrixPage(Page): form_model = 'player' template_name = 'raven_matrices/Matrix.html' @staticmethod def get_form_fields(player): return [f'answer_{matrix_idx}'] @staticmethod def vars_for_template(player): pos = C.MATRIX_INDICES.index(matrix_idx) + 1 return dict( matrix_number=matrix_idx, matrix_position=pos, image_path=f'raven_matrices/{matrix_idx}.png', total=C.NUM_MATRICES, ) DynamicMatrixPage.__name__ = f'Matrix_{matrix_idx}' DynamicMatrixPage.__qualname__ = f'Matrix_{matrix_idx}' return DynamicMatrixPage # Generate page classes only for selected matrices _matrix_pages = [_make_matrix_page(i) for i in C.MATRIX_INDICES] page_sequence = [Introduction] + _matrix_pages + [Results]