import csv import re from pathlib import Path from secrets import SystemRandom from otree.api import * secure_random = SystemRandom() TRIAL_FIELDS = [ 'source_unique_id', 'game_id', 'source_up_choice', 'source_row_form_matrix', 'source_topology', 'presented_row_topology', 'presented_col_topology', 'selected_level', ] def read_games(): games = {} csv_path = Path(__file__).with_name( 'step-02-game-schedule_output-01-rowplayer-complete-games.csv' ) with csv_path.open(encoding='utf-8-sig', newline='') as file: for row in csv.DictReader(file): matrix = [int(value) for value in re.findall(r'-?\d+', row['row_form_matrix'])] r0, r1, r2, r3, c0, c1, c2, c3 = matrix unique_id = int(row['unique_id']) games[unique_id] = dict( source_unique_id=unique_id, game_id=int(row['game_id']), source_up_choice=None if row['up_choice'] == 'NA' else float(row['up_choice']), source_row_form_matrix=row['row_form_matrix'], source_topology=row['topology'], presented_row_topology=row['col_topology'], presented_col_topology=row['row_topology'], selected_level=row['probability_level'], self_c_a=r0, other_c_a=c0, self_c_b=r2, other_c_b=c2, self_d_a=r1, other_d_a=c1, self_d_b=r3, other_d_b=c3, ) return games def read_schedules(): csv_path = Path(__file__).with_name( 'step-02-game-schedule_output-02-subject-schedules.csv' ) with csv_path.open(encoding='utf-8-sig', newline='') as file: return { int(row['schedule_id']): [ int(row[column]) for column in row if column.startswith('game_') ] for row in csv.DictReader(file) } GAMES = read_games() SCHEDULES = read_schedules() class C(BaseConstants): NAME_IN_URL = 'matrix_game' PLAYERS_PER_GROUP = None NUM_ROUNDS = len(next(iter(SCHEDULES.values()))) JITTER_MIN_MS = 200 JITTER_MAX_MS = 600 EXAMPLE_MATRIX = dict( self_top_left=11, other_top_left=11, self_top_right=6, other_top_right=4, self_bottom_left=4, other_bottom_left=6, self_bottom_right=6, other_bottom_right=6, ) class Subsession(BaseSubsession): pass class ScheduleAssignment(ExtraModel): session_code = models.StringField() participant_code = models.StringField() schedule_id = models.IntegerField() finished = models.BooleanField(initial=False) def creating_session(subsession): if subsession.round_number == 1: records = ScheduleAssignment.filter() attempted_ids = {record.schedule_id for record in records} completed_ids = {record.schedule_id for record in records if record.finished} unfinished_ids = sorted(attempted_ids - completed_ids) unused_ids = sorted(set(SCHEDULES) - attempted_ids) available_ids = unfinished_ids + unused_ids players = subsession.get_players() if len(players) > len(available_ids): raise RuntimeError('Not enough unfinished or unused schedules for this session.') for player, schedule_id in zip(players, available_ids): participant = player.participant trials = [GAMES[unique_id] for unique_id in SCHEDULES[schedule_id]] secure_random.shuffle(trials) participant.finished = False participant.schedule_id = schedule_id participant.vars['matrix_game_trials'] = trials ScheduleAssignment.create( session_code=subsession.session.code, participant_code=participant.code, schedule_id=schedule_id, finished=False, ) for player in subsession.get_players(): trial = player.participant.vars['matrix_game_trials'][player.round_number - 1] player.schedule_id = player.participant.schedule_id for field in TRIAL_FIELDS: setattr(player, field, trial[field]) row_options = ['C', 'D'] col_options = ['A', 'B'] secure_random.shuffle(row_options) secure_random.shuffle(col_options) player.top_option = row_options[0] player.left_option = col_options[0] for row_position, row_option in zip(('top', 'bottom'), row_options): for col_position, col_option in zip(('left', 'right'), col_options): option_pair = f'{row_option.lower()}_{col_option.lower()}' position = f'{row_position}_{col_position}' setattr(player, f'self_{position}', trial[f'self_{option_pair}']) setattr(player, f'other_{position}', trial[f'other_{option_pair}']) player.jitter_ms = secure_random.randint(C.JITTER_MIN_MS, C.JITTER_MAX_MS) class Group(BaseGroup): pass class Player(BasePlayer): schedule_id = models.IntegerField() rt = models.FloatField(blank=True) chosen_side = models.StringField(choices=['Left', 'Right'], blank=True) chosen_option = models.StringField(choices=['A', 'B'], blank=True) missed = models.BooleanField(initial=False) jitter_ms = models.IntegerField() source_unique_id = models.IntegerField() game_id = models.IntegerField() source_up_choice = models.FloatField(blank=True) source_row_form_matrix = models.LongStringField() source_topology = models.StringField() presented_row_topology = models.StringField() presented_col_topology = models.StringField() selected_level = models.StringField() top_option = models.StringField(choices=['C', 'D']) left_option = models.StringField(choices=['A', 'B']) self_top_left = models.IntegerField() other_top_left = models.IntegerField() self_top_right = models.IntegerField() other_top_right = models.IntegerField() self_bottom_left = models.IntegerField() other_bottom_left = models.IntegerField() self_bottom_right = models.IntegerField() other_bottom_right = models.IntegerField() check1 = models.StringField( choices=[ ('A', 'A random computer choice'), ('B', "My Left/Right choice and a future row player's Top/Bottom choice"), ('C', 'Only my Left/Right choice'), ], label='1. What determines the points for a selected game?', widget=widgets.RadioSelect, ) check2 = models.StringField( choices=[ ('A', 'The computer chooses Top or Bottom at random'), ('B', 'A participant currently making a choice at the same time as me'), ('C', 'One or more new participants who will respond as row players in later experiments'), ], label='2. Who may make the row-player choices paired with your response?', widget=widgets.RadioSelect, ) check3 = models.StringField( choices=[ ('A', 'No, each response can be used only once'), ('B', 'Yes, it may be used in more than one future experiment and paired more than once'), ('C', 'Only if I make the same choice repeatedly'), ], label='3. Can one of your responses be used and paired more than once?', widget=widgets.RadioSelect, ) check4 = models.StringField( choices=[ ('A', 'They will see my name and contact details'), ('B', 'They will see my Prolific ID'), ('C', 'They will receive no information that can identify me'), ], label='4. What personal information about you will future matched participants receive?', widget=widgets.RadioSelect, ) check5 = models.StringField( choices=[('A', '6 points'), ('B', '11 points'), ('C', '4 points')], label='5. In the example, if you choose Left and the row player chooses Bottom, how many points do you receive?', widget=widgets.RadioSelect, ) check6 = models.StringField( choices=[('A', '4 points'), ('B', '6 points'), ('C', '11 points')], label='6. In the same case, how many points does the row player receive?', widget=widgets.RadioSelect, ) class MatrixInstructions(Page): @staticmethod def is_displayed(player): return player.round_number == 1 class MatrixExample(Page): @staticmethod def is_displayed(player): return player.round_number == 1 @staticmethod def vars_for_template(player): return C.EXAMPLE_MATRIX class MatrixCheck(Page): form_model = 'player' form_fields = ['check1', 'check2', 'check3', 'check4', 'check5', 'check6'] @staticmethod def is_displayed(player): return player.round_number == 1 @staticmethod def vars_for_template(player): return C.EXAMPLE_MATRIX @staticmethod def error_message(player, values): correct_answers = dict( check1='B', check2='C', check3='B', check4='C', check5='C', check6='B', ) if any(values[field] != answer for field, answer in correct_answers.items()): return 'At least one answer is incorrect. Please try again.' class MatrixTask(Page): form_model = 'player' form_fields = ['chosen_side', 'rt'] @staticmethod def vars_for_template(player): return dict( self_top_left=player.self_top_left, other_top_left=player.other_top_left, self_top_right=player.self_top_right, other_top_right=player.other_top_right, self_bottom_left=player.self_bottom_left, other_bottom_left=player.other_bottom_left, self_bottom_right=player.self_bottom_right, other_bottom_right=player.other_bottom_right, ) @staticmethod def before_next_page(player, timeout_happened): player.chosen_option = ( player.left_option if player.chosen_side == 'Left' else ('B' if player.left_option == 'A' else 'A') ) if player.round_number == C.NUM_ROUNDS: participant = player.participant participant.finished = True assignment, = [ record for record in ScheduleAssignment.filter() if record.session_code == player.session.code and record.participant_code == participant.code ] assignment.finished = True class MatrixPracticeIntro(Page): @staticmethod def is_displayed(player): return player.round_number == 1 class MatrixPractice(Page): @staticmethod def is_displayed(player): return player.round_number == 1 @staticmethod def vars_for_template(player): return C.EXAMPLE_MATRIX class MatrixPracticeEnd(Page): @staticmethod def is_displayed(player): return player.round_number == 1 class Fixation(Page): @staticmethod def is_displayed(player): return player.round_number < C.NUM_ROUNDS class MatrixEnd(Page): @staticmethod def is_displayed(player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player): return dict( prolific_completion_url=player.session.vars.get('prolific_completion_url') ) page_sequence = [ MatrixInstructions, MatrixExample, MatrixCheck, MatrixPracticeIntro, MatrixPractice, MatrixPracticeEnd, MatrixTask, Fixation, MatrixEnd, ] def custom_export(players): yield ['session_code', 'participant_code', 'schedule_id', 'finished'] for record in ScheduleAssignment.filter(): yield [ record.session_code, record.participant_code, record.schedule_id, record.finished, ]