from otree.api import * c = cu doc = '' class C(BaseConstants): NAME_IN_URL = 'trinarygrid_scroll' PLAYERS_PER_GROUP = None NUM_ROUNDS = 120 BELOWFOLD = True VALUEDIFF0 = True class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): prolificcode = models.StringField() screensize = models.StringField() gender = models.StringField(label=' ', widget=widgets.RadioSelect, choices=[['male', 'male'], ['female', 'female']]) choice = models.StringField() BCArounds = models.StringField() condition = models.StringField() invertedrounds = models.StringField() notdisplayed = models.StringField() time = models.StringField(initial='') y = models.StringField(initial='') slotseq = models.StringField() time_startscrolling = models.FloatField() time_endscrolling = models.FloatField() rt = models.FloatField() scrolltime = models.FloatField() shoepairs = models.StringField() shoeorder = models.StringField() shoeratings = models.StringField() terminate = models.BooleanField(initial=False) thisround_shoepair = models.StringField() thisround_thirdshoe = models.StringField() thisround_shoeArating = models.IntegerField() thisround_shoeBrating = models.IntegerField() thisround_shoeCrating = models.IntegerField() thisround_valuedifference = models.IntegerField() def shoeid(player: Player): maleshoes = [ 15147403, 12100503, 21001440, 12126140, 15133340, 16105627, 11798240, 12000002, 21228230, 15139716, 13149303, 11028927, 16105644, 16105601, 13138103, 12134602, 13149026, 16105699, 16105621, 12126103, 12254302, 12100540, 21001403, 16106103, 16105640, 11798203, 12000041, 16105642, 13149340, 13149041, 13123926, 15153203, 21153118, 13141627, 12000001, 12141001, 13149003, 13107203, 13149027, 13107227, 13123940, 12131399, 13141603, 12126143, 13149018, 12102121, 12105399, 12141003, 12105340, 13107201, 16105602, 13141625, 12000003, 21228208, 12131303, 13107240, 16105683, 12105301, 12100522, 13149040, 12126119, 12100402, 12257527, 13141640, 13123903, 13149099, 15146618, 12126147, 15112718, 15395841, 13149103, 12102103, 12000040, 13110730, 21060740, 12105303, 13110701, 16105608, 15148701, 12237827, 21005915, 13110703, 21010103, 15395740, 12237803, 15111704, 11795703, 13138640, 15101627, 13153740, 12126172, 12887143, 13145640, 21060826, 21073718, 13149032, 13106703, 15111706, 13149069, 12672703, 13100640, 15156141, 13110702, 12141037, 13149072, 21073501, 15148724, 12106926, 12672740, 13149033, 12141074, 15156201, 12887140, 13117908, 11028930] # remove duplicates #maleshoes = list(set(maleshoes)) femaleshoes = [ 27419401, 27638735, 27434101, 21001315, 21600103, 27656407, 27198530, 27372703, 21025170, 21061403, 27400306, 21107306, 27656440, 27434103, 27419403, 27428801, 27434182, 27400001, 27306676, 27307003, 27752137, 21000830, 27656403, 27400003, 21001215, 21025131, 27434140, 27656441, 27400340, 27111302, 27209840, 27652203, 27638734, 27301903, 27198531, 27306630, 21061406, 21001334, 27410903, 27652240, 27652141, 27458001, 21040803, 21505203, 21084273, 21104208, 27638732, 21061420, 21001703, 27410901, 21153203, 27316903, 21107303, 21484635, 27400301, 21001307, 27400330, 21061470, 27306603, 27752107, 21025103, 21025127, 21001203, 27656415, 27434170, 27209832, 27306601, 27410932, 27111308, 21001340, 21025105, 27752101, 21000806, 27434131, 21084203, 27306621, 21025003, 21061119, 21000804, 27400309, 21025027, 27415927, 27752103, 27417603, 27209833, 21084206, 21061003, 21001099, 27106742, 27112103, 27234801, 21153231, 21001608, 21025102, 27306602, 27234803, 21025022, 27458332, 27106740, 27112101, 27434132, 27434177, 27142503, 27333540, 27198501, 21104227, 27234840, 27372203, 21001319, 21072403, 27401667, 21025120, 21104203, 27106703, 27647240] # remove duplicates #femaleshoes = list(set(femaleshoes)) # remove non-images #non = set([27301931,21001321,13141641,27656431,13123925,21061470,21001612,21153234]) #maleshoes = [item for item in maleshoes if item not in non] #femaleshoes = [item for item in femaleshoes if item not in non] # use the first 90 maleshoes = maleshoes[0:90] femaleshoes = femaleshoes[0:90] if player.in_round(1).gender == 'male': return maleshoes else: return femaleshoes class MobileCheck(Page): form_model = 'player' form_fields = ['screensize'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): session = player.session return dict( pilot = session.config['pilot'] ) class Information(Page): form_model = 'player' form_fields = ['prolificcode'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Instructions(Page): form_model = 'player' form_fields = ['gender'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def before_next_page(player: Player, timeout_happened): ### # Randomise the order of shoes ## import random maleshoes = shoeid(player) femaleshoes = shoeid(player) random.shuffle(maleshoes) random.shuffle(femaleshoes) maleshoes = [eval(str(i)) for i in maleshoes] femaleshoes = [eval(str(i)) for i in femaleshoes] if player.gender == 'male': player.shoeorder = str(maleshoes) else: player.shoeorder = str(femaleshoes) class Preference(Page): form_model = 'player' form_fields = ['shoeratings'] @staticmethod def is_displayed(player: Player): session = player.session return player.round_number == 1 and player.session.config['pilot'] == 0 @staticmethod def vars_for_template(player: Player): import ast shoeorder = ast.literal_eval(player.shoeorder) divs = '' for i in range(len(shoeorder)): #divs += 'shoe' + str(i) + ': ' + # used for testing divs += '' return dict( divs = divs, shoeorder = shoeorder ) @staticmethod def js_vars(player: Player): session = player.session import ast shoeorder = ast.literal_eval(player.shoeorder) if player.gender == 'male': imagelist = ["https://users.aalto.fi/ileppane/shoepictures2026/men/" + str(x) + "_h.jpg" for x in shoeorder] else: imagelist = ["https://users.aalto.fi/ileppane/shoepictures2026/women/" + str(x) + "_h.jpg" for x in shoeorder] return dict( numberofshoes = len(shoeorder), shoeorder = shoeorder, imagelist = imagelist, pilot = player.session.config['pilot'] ) @staticmethod def before_next_page(player: Player, timeout_happened): session = player.session import random import copy import ast ### # Here the pairs are created based on preference ratings ### shoeorder = ast.literal_eval(player.shoeorder) shoeratings = ast.literal_eval(player.shoeratings) # make three copies of shoeorder and shuffle them shoes1 = shoeorder shoes2 = copy.deepcopy(shoeorder) random.shuffle(shoes2) # make a long list (length 90^2) of all possible pairs and shuffle longpairs = [[x, y] for x in shoes1 for y in shoes2] random.shuffle(longpairs) pairs = [] # final pair list i = 0 while len(pairs) < C.NUM_ROUNDS and i < 8000: pair = longpairs[i] ratingA = shoeratings[shoeorder.index(pair[0])] ratingB = shoeratings[shoeorder.index(pair[1])] valuediff = ratingA - ratingB # A is always dominant # ensure that there are shoes that are strictly lower-valued than the ones in the pair if C.VALUEDIFF0 == True: # valuediff always 0 if pair[0] != pair[1] and abs(valuediff) == 0 and ratingB > min(shoeratings): pairs.append(pair) else: # valuediff always 1 if pair[0] != pair[1] and abs(valuediff) == 1 and ratingA > ratingB and ratingA > min(shoeratings) and ratingB > min(shoeratings): pairs.append(pair) # valuediff can be 1 or 2 #if pair[0] != pair[1] and (abs(valuediff) == 1 or abs(valuediff) == 2) and ratingA > ratingB and ratingA > min(shoeratings) and ratingB > min(shoeratings): pairs.append(pair) i += 1 random.shuffle(pairs) # if the algorithm has failed to create the pair list, then terminate the study if player.session.config['pilot'] == 0 and len(pairs) < C.NUM_ROUNDS: player.terminate = True # the pair for each shoe can now be found in this variable: player.in_round(1).shoepairs player.shoepairs = str(pairs) class PreferencePilot(Page): form_model = 'player' timeout_seconds = 1 @staticmethod def is_displayed(player: Player): session = player.session return player.session.config['pilot'] == 1 and player.round_number == 1 @staticmethod def before_next_page(player: Player, timeout_happened): session = player.session import random import copy import ast ### # use test data for piloting ### player_shoeorder = '[15147403, 12100503, 21001440, 12126140, 15133340, 16105627, 11798240, 12000002, 21228230, 15139716, 13149303, 11028927, 16105644, 16105601, 13138103, 12134602, 13149026, 16105699, 16105621, 12126103, 12254302, 12100540, 21001403, 16106103, 16105640, 11798203, 12000041, 16105642, 13149340, 13149041, 13123926, 15153203, 21153118]' player_shoeratings = '[5,6,5,6,6,5,5,5,5,6,6,5,5,5,5,5,5,5,6,3,5,5,5,6,4,6,5,5,6,6,5,6,6]' player.shoeorder = player_shoeorder player.shoeratings = player_shoeratings shoeorder = ast.literal_eval(player.shoeorder) shoeratings = ast.literal_eval(player.shoeratings) # make three copies of shoeorder and shuffle them shoes1 = shoeorder shoes2 = copy.deepcopy(shoeorder) random.shuffle(shoes2) # make a long list (length 90^2) of all possible pairs and shuffle longpairs = [[x, y] for x in shoes1 for y in shoes2] random.shuffle(longpairs) pairs = [] # final pair list i = 0 while len(pairs) < C.NUM_ROUNDS and i < 8000: pair = longpairs[i] ratingA = shoeratings[shoeorder.index(pair[0])] ratingB = shoeratings[shoeorder.index(pair[1])] valuediff = ratingA - ratingB # A is always dominant # ensure that there are shoes that are strictly lower-valued than the ones in the pair if C.VALUEDIFF0 == True: # valuediff always 0 if pair[0] != pair[1] and abs(valuediff) == 0 and ratingB > min(shoeratings): pairs.append(pair) else: # valuediff always 1 if pair[0] != pair[1] and abs(valuediff) == 1 and ratingA > ratingB and ratingA > min(shoeratings) and ratingB > min(shoeratings): pairs.append(pair) #if pair[0] != pair[1] and (abs(valuediff) == 1 or abs(valuediff) == 2) and ratingA > ratingB and ratingA > min(shoeratings) and ratingB > min(shoeratings): pairs.append(pair) i += 1 random.shuffle(pairs) # if the algorithm has failed to create the pair list, then terminate if player.session.config['pilot'] == 0 and len(pairs) < C.NUM_ROUNDS: player.terminate = True # the pair for each shoe can now be found in this variable: player.in_round(1).shoepairs player.shoepairs = str(pairs) class Instructions2(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): session = player.session return dict( terminate = player.terminate, prolificurl = player.session.config['prolificurl'] ) @staticmethod def before_next_page(player: Player, timeout_happened): import random # determine which rounds will be BCA and which CBA condition player.BCArounds = str(random.sample(range(C.NUM_ROUNDS), int(C.NUM_ROUNDS/2))) # determine which rounds will have the order of alternatives inverted # (e.g. if BCA round then order is ACB) player.invertedrounds = str(random.sample(range(C.NUM_ROUNDS), int(C.NUM_ROUNDS/2))) class PreChoice(Page): form_model = 'player' timeout_seconds = 1 @staticmethod def before_next_page(player: Player, timeout_happened): # Determine the pair of shoes for this round and calculate their ratings and valuedifference import ast, random BCArounds = ast.literal_eval(player.in_round(1).BCArounds) invertedrounds = ast.literal_eval(player.in_round(1).invertedrounds) if player.round_number in BCArounds: player.condition = 'BCA' else: player.condition = 'CBA' if player.round_number in invertedrounds: player.notdisplayed = 'shoe3' else: player.notdisplayed = 'shoe2' shoepairs = ast.literal_eval(player.in_round(1).shoepairs) thisround_shoepair = shoepairs[player.round_number-1] shoeorder = ast.literal_eval(player.in_round(1).shoeorder) shoeratings = ast.literal_eval(player.in_round(1).shoeratings) # ratings = subjective preferences givien by player player.thisround_shoepair = str(thisround_shoepair) player.thisround_shoeArating = int(shoeratings[shoeorder.index(thisround_shoepair[0])]) player.thisround_shoeBrating = int(shoeratings[shoeorder.index(thisround_shoepair[1])]) player.thisround_valuedifference = player.thisround_shoeArating - player.thisround_shoeBrating # Randomly find a third shoe while player.field_maybe_none('thisround_thirdshoe') is None: thirdshoe = random.choice(shoeorder) rating = int(shoeratings[shoeorder.index(thirdshoe)]) # ensure that rating difference between 2nd and 3rd is always 1 if rating == player.thisround_shoeBrating - 1 and thirdshoe != thisround_shoepair[0] and thirdshoe != thisround_shoepair[1]: player.thisround_thirdshoe = str(thirdshoe) player.thisround_shoeCrating = rating class ChoiceBelowFold(Page): form_model = 'player' form_fields = ['choice', 'time', 'y', 'time_startscrolling', 'time_endscrolling', 'rt'] @staticmethod def is_displayed(player: Player): return C.BELOWFOLD @staticmethod def js_vars(player: Player): import ast shoepair = ast.literal_eval(player.thisround_shoepair) player1 = player.in_round(1) if player1.gender == 'male': url = 'https://users.aalto.fi/ileppane/shoepictures2026/men/' else: url = 'https://users.aalto.fi/ileppane/shoepictures2026/women/' return dict( condition = player.condition, notdisplayed = player.notdisplayed, shoeimageA = url + str(shoepair[0]) + '_h.jpg', shoeimageB = url + str(shoepair[1]) + '_h.jpg', shoeimageC = url + str(player.thisround_thirdshoe) + '_h.jpg' ) @staticmethod def before_next_page(player: Player, timeout_happened): player.scrolltime = player.time_endscrolling - player.time_startscrolling class ChoiceNoFold(Page): form_model = 'player' form_fields = ['y', 'time', 'time_startscrolling', 'time_endscrolling', 'choice', 'slotseq', 'rt'] @staticmethod def is_displayed(player: Player): return C.BELOWFOLD == False @staticmethod def js_vars(player: Player): import ast shoepair = ast.literal_eval(player.thisround_shoepair) player1 = player.in_round(1) if player1.gender == 'male': url = 'https://users.aalto.fi/ileppane/shoepictures2026/men/' else: url = 'https://users.aalto.fi/ileppane/shoepictures2026/women/' return dict( condition = player.condition, notdisplayed = player.notdisplayed, shoeimageA = url + str(shoepair[0]) + '_h.jpg', shoeimageB = url + str(shoepair[1]) + '_h.jpg', shoeimageC = url + str(player.thisround_thirdshoe) + '_h.jpg' ) @staticmethod def before_next_page(player: Player, timeout_happened): player.scrolltime = player.time_endscrolling - player.time_startscrolling class Results(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): session = player.session return dict( prolificurl = player.session.config['prolificurl'] ) class MidPage(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): display = False if C.NUM_ROUNDS == 120 and player.round_number in [30,60,90]: # this is for prolific pilots display = True if C.NUM_ROUNDS == 180 and player.round_number in [30,60,90,120,150]: # this is for real data collection display = True return display page_sequence = [MobileCheck, Information, Instructions, Preference, PreferencePilot, Instructions2, PreChoice, ChoiceBelowFold, ChoiceNoFold, Results, MidPage]