from otree.api import * import random class C(BaseConstants): NAME_IN_URL = 'two_player_gg' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 20 alpha = 3/4 beta0 = 21 beta1 = 3.5 R = 1000 c_payoff = 0.05 ROUND_SHOCK = 11 PRE_SHOCK_ROUND = ROUND_SHOCK - 1 conversion_rate = 0.002 SHOWUP = 4 class Group(BaseGroup): pass class Subsession(BaseSubsession): def set_payoffs(self): players = self.get_players() a_values = [p.a for p in players] for player in players: # Calculate bin_shares for payoff_first bin_shares = calculate_bin_shares(a_values, players, player) player.payoff_first = compute_payoff_first(player, a_values, bin_shares) # Calculate other_bfirst_values for payoff_second player.id_partner, other_bfirst_values = get_random_participant_bfirst_values(player) player.payoff_second = compute_payoff_second(player, other_bfirst_values) # Add payoffs player.payoff = player.payoff_first + player.payoff_second class Player(BasePlayer): a = models.FloatField( label='What number do you choose?', min=0, max=100 ) a_opponent = models.FloatField() bfirst_1 = models.IntegerField(min=0, max=100, label='0-4', blank=True) bfirst_2 = models.IntegerField(min=0, max=100, label='5-9', blank=True) bfirst_3 = models.IntegerField(min=0, max=100, label='10-14', blank=True) bfirst_4 = models.IntegerField(min=0, max=100, label='15-19', blank=True) bfirst_5 = models.IntegerField(min=0, max=100, label='20-24', blank=True) bfirst_6 = models.IntegerField(min=0, max=100, label='25-29', blank=True) bfirst_7 = models.IntegerField(min=0, max=100, label='30-34', blank=True) bfirst_8 = models.IntegerField(min=0, max=100, label='35-39', blank=True) bfirst_9 = models.IntegerField(min=0, max=100, label='40-44', blank=True) bfirst_10 = models.IntegerField(min=0, max=100, label='45-49', blank=True) bfirst_11 = models.IntegerField(min=0, max=100, label='50-54', blank=True) bfirst_12 = models.IntegerField(min=0, max=100, label='55-59', blank=True) bfirst_13 = models.IntegerField(min=0, max=100, label='60-64', blank=True) bfirst_14 = models.IntegerField(min=0, max=100, label='65-69', blank=True) bfirst_15 = models.IntegerField(min=0, max=100, label='70-74', blank=True) bfirst_16 = models.IntegerField(min=0, max=100, label='75-79', blank=True) bfirst_17 = models.IntegerField(min=0, max=100, label='80-84', blank=True) bfirst_18 = models.IntegerField(min=0, max=100, label='85-89', blank=True) bfirst_19 = models.IntegerField(min=0, max=100, label='90-94', blank=True) bfirst_20 = models.IntegerField(min=0, max=100, label='95-100', blank=True) bsecond_1 = models.IntegerField(min=0, max=100, label='0-4', blank=True) bsecond_2 = models.IntegerField(min=0, max=100, label='5-9', blank=True) bsecond_3 = models.IntegerField(min=0, max=100, label='10-14', blank=True) bsecond_4 = models.IntegerField(min=0, max=100, label='15-19', blank=True) bsecond_5 = models.IntegerField(min=0, max=100, label='20-24', blank=True) bsecond_6 = models.IntegerField(min=0, max=100, label='25-29', blank=True) bsecond_7 = models.IntegerField(min=0, max=100, label='30-34', blank=True) bsecond_8 = models.IntegerField(min=0, max=100, label='35-39', blank=True) bsecond_9 = models.IntegerField(min=0, max=100, label='40-44', blank=True) bsecond_10 = models.IntegerField(min=0, max=100, label='45-49', blank=True) bsecond_11 = models.IntegerField(min=0, max=100, label='50-54', blank=True) bsecond_12 = models.IntegerField(min=0, max=100, label='55-59', blank=True) bsecond_13 = models.IntegerField(min=0, max=100, label='60-64', blank=True) bsecond_14 = models.IntegerField(min=0, max=100, label='65-69', blank=True) bsecond_15 = models.IntegerField(min=0, max=100, label='70-74', blank=True) bsecond_16 = models.IntegerField(min=0, max=100, label='75-79', blank=True) bsecond_17 = models.IntegerField(min=0, max=100, label='80-84', blank=True) bsecond_18 = models.IntegerField(min=0, max=100, label='85-89', blank=True) bsecond_19 = models.IntegerField(min=0, max=100, label='90-94', blank=True) bsecond_20 = models.IntegerField(min=0, max=100, label='95-100', blank=True) target = models.FloatField() payoff_guessing = models.FloatField() payoff_first = models.FloatField(default=0) payoff_second = models.FloatField(default=0) id_partner = models.IntegerField() tot_payoff_guessing_2pg = models.FloatField() tot_payoff_first_2pg = models.FloatField() tot_payoff_second_2pg = models.FloatField() tot_payoff_2pg = models.FloatField() final_payoff = models.FloatField() final_payoff_in_euro = models.FloatField() payoff_plus_showup = models.FloatField() def compute_payoffs_and_targets(self): other_player = self.get_others_in_group()[0] self.a_opponent = other_player.a if self.round_number < C.ROUND_SHOCK: beta = C.beta0 else: beta = C.beta1 self.target = C.alpha * other_player.a + beta self.payoff_guessing = round(max((1 - C.c_payoff * abs(self.a - self.target)) * 1500, 0)) # def compute_total_payoffs(self): # self.tot_payoff_guessing_2pg = round(sum(p.payoff_guessing for p in self.in_all_rounds())) # self.tot_payoff_first_2pg = round(sum(p.payoff_first for p in self.in_all_rounds())) # self.tot_payoff_second_2pg = round(sum(p.payoff_second for p in self.in_all_rounds())) # self.tot_payoff_2pg = round(self.tot_payoff_guessing_2pg + self.tot_payoff_first_2pg + self.tot_payoff_second_2pg) # self.participant.vars['two_player_gg'] = self.tot_payoff_2pg def compute_total_payoffs(self): all_rounds = self.in_all_rounds() # Select a random round for payoff_guessing random_round_guessing = random.choice(all_rounds) # Select either round 1 or round 11 for payoff_first and payoff_second random_round_first = random.choice([all_rounds[0], all_rounds[C.PRE_SHOCK_ROUND]]) random_round_second = random.choice([all_rounds[0], all_rounds[C.PRE_SHOCK_ROUND]]) self.tot_payoff_guessing_2pg = round(random_round_guessing.payoff_guessing) self.tot_payoff_first_2pg = round(random_round_first.payoff_first) self.tot_payoff_second_2pg = round(random_round_second.payoff_second) self.tot_payoff_2pg = round( self.tot_payoff_guessing_2pg + self.tot_payoff_first_2pg + self.tot_payoff_second_2pg) self.participant.vars['two_player_gg'] = self.tot_payoff_2pg def calculate_final_payoff(self): #print(f"Participant vars (survey): {self.participant.vars.get('survey', 'Not set')}") final_payoff = ( self.participant.vars['survey'] + #self.participant.vars['one_player_gg'] + self.participant.vars['two_player_gg'] ) #self.part_one_payoff = #self.part_two_payoff #self.part_three_payoff self.final_payoff = final_payoff self.final_payoff_in_euro = round(final_payoff * C.conversion_rate,2) self.payoff_plus_showup = round((final_payoff)*C.conversion_rate+C.SHOWUP,2) def get_bin_index(a_value): if a_value == 100: return 20 return (a_value // 5) + 1 def calculate_bin_shares(a_values, players, player): #a_values = [p.a for p in players] total_players = len(players) - 1 # Subtract 1 to exclude the player's own choice bin_shares = [0] * 20 for i, a_value in enumerate(a_values): bin_index = int(get_bin_index(a_value)) - 1 bin_shares[bin_index] += 1 # Remove the player's own choice player_bin_index = int(get_bin_index(player.a)) - 1 bin_shares[player_bin_index] -= 1 bin_shares = [x / total_players for x in bin_shares] return bin_shares def compute_payoff_first(player, a_values, bin_shares): bfirst_values = [player.field_maybe_none(f'bfirst_{i}') or 0 for i in range(1, 21)] payoff_first = 2 for i, p_i in enumerate(bin_shares): penalty = (p_i * sum((bfirst_values[j]/100 - (1 if j == i else 0)) ** 2 for j in range(20))) #print(f'Penalty for index {i}: {penalty}') # Display the penalty value payoff_first -= penalty payoff_first *= 750 # Multiply payoff by 750 to have a max payoff of 1500 per guess return round(payoff_first) # Round the result and return it def get_random_participant_bfirst_values(current_player): # Get the players in the same subsession (round) as the current player other_players = [player for player in current_player.subsession.get_players() if player.participant.id_in_session != current_player.participant.id_in_session] selected_player = random.choice(other_players) bfirst_values = [selected_player.field_maybe_none(f'bfirst_{i}') or 0 for i in range(1, 21)] return selected_player.id_in_group, bfirst_values def compute_payoff_second(player, other_bfirst_values): bsecond_values = [player.field_maybe_none(f'bsecond_{i}') or 0 for i in range(1, 21)] payoff_second = 2 for i, other_p_i in enumerate(other_bfirst_values): penalty = (other_p_i / 100 * sum((bsecond_values[j] / 100 - (1 if j == i else 0)) ** 2 for j in range(20))) #print(f'Penalty for index {i}: {penalty}') # Add this line to display the penalty value payoff_second -= penalty payoff_second *= 750 # Multiply payoff by 750 to have a max payoff of 1500 per guess return round(payoff_second) # Round the result and return it class PartThreeBegins(Page): pass @staticmethod def is_displayed(self): return self.round_number == 1 #class InstructionsPartThree(Page): # pass # @staticmethod # def is_displayed(self): # return self.round_number == 1 class GuessingGame(Page): form_model = 'player' form_fields = ['a'] def is_displayed(self): return self.round_number not in [1, C.ROUND_SHOCK] def vars_for_template(self): return { 'round_number': self.round_number, } class Beliefs(Page): form_model = 'player' form_fields = [f'bfirst_{i}' for i in range(1, 21)] template_name = "two_player_gg/Beliefs.html" def vars_for_template(self): return { 'round_number': self.round_number, } @staticmethod def error_message(player, values): print('values is', values) total = ( (values.get('bfirst_1') or 0) + (values.get('bfirst_2') or 0) + (values.get('bfirst_3') or 0) + (values.get('bfirst_4') or 0) + (values.get('bfirst_5') or 0) + (values.get('bfirst_6') or 0) + (values.get('bfirst_7') or 0) + (values.get('bfirst_8') or 0) + (values.get('bfirst_9') or 0) + (values.get('bfirst_10') or 0) + (values.get('bfirst_11') or 0) + (values.get('bfirst_12') or 0) + (values.get('bfirst_13') or 0) + (values.get('bfirst_14') or 0) + (values.get('bfirst_15') or 0) + (values.get('bfirst_16') or 0) + (values.get('bfirst_17') or 0) + (values.get('bfirst_18') or 0) + (values.get('bfirst_19') or 0) + (values.get('bfirst_20') or 0) ) if total != 100: return 'The numbers must add up to 100' def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] #def before_next_page(player, timeout_happened): #bfirst_values = [player.field_maybe_none(f'bfirst_{i}') or 0 for i in range(1, 21)] #player.bfirst_values = ','.join(map(str, bfirst_values)) class BeliefsTwo(Page): form_model = 'player' form_fields = [f'bsecond_{i}' for i in range(1, 21)] template_name = "two_player_gg/BeliefsTwo.html" def vars_for_template(self): return { 'round_number': self.round_number, } @staticmethod def error_message(player, values): print('values is', values) total = ( (values.get('bsecond_1') or 0) + (values.get('bsecond_2') or 0) + (values.get('bsecond_3') or 0) + (values.get('bsecond_4') or 0) + (values.get('bsecond_5') or 0) + (values.get('bsecond_6') or 0) + (values.get('bsecond_7') or 0) + (values.get('bsecond_8') or 0) + (values.get('bsecond_9') or 0) + (values.get('bsecond_10') or 0) + (values.get('bsecond_11') or 0) + (values.get('bsecond_12') or 0) + (values.get('bsecond_13') or 0) + (values.get('bsecond_14') or 0) + (values.get('bsecond_15') or 0) + (values.get('bsecond_16') or 0) + (values.get('bsecond_17') or 0) + (values.get('bsecond_18') or 0) + (values.get('bsecond_19') or 0) + (values.get('bsecond_20') or 0) ) if total != 100: return 'The numbers must add up to 100' def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] class GuessingGameWaitPage(WaitPage): pass class GroupResultsWaitPage(WaitPage): def after_all_players_arrive(self): for player in self.group.get_players(): player.compute_payoffs_and_targets() class WaitForChoices(WaitPage): wait_for_all_groups = True def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] # class WaitForBeliefs(WaitPage): # wait_for_all_groups = True # # def is_displayed(self): # return self.round_number in [1, C.ROUND_SHOCK] class ShockOccurs(Page): pass @staticmethod def is_displayed(self): return self.round_number == C.ROUND_SHOCK class SessionResultsWaitPage(WaitPage): wait_for_all_groups = True def after_all_players_arrive(self): subsession = self.subsession subsession.set_payoffs() # Add this line #for player in subsession.get_players(): # player.compute_payoffs_and_targets() def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] class ShowResults(Page): wait_for_all_groups = True def vars_for_template(player: Player): return { 'payoff_guessing': player.payoff_guessing, 'a': player.a, 'a_opponent': player.a_opponent, 'payoff_first': player.payoff_first, 'payoff_second': player.payoff_second, 'round_number': player.round_number } def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] class RoundResults(Page): def vars_for_template(player: Player): return { 'payoff_guessing': player.payoff_guessing, 'a': player.a, 'a_opponent': player.a_opponent, 'round_number': player.round_number } #def is_displayed(self): # return self.round_number not in [1, C.ROUND_SHOCK] class WaitForFinalPage(WaitPage): def is_displayed(self): return self.round_number == C.NUM_ROUNDS def after_all_players_arrive(self): for player in self.group.get_players(): player.compute_total_payoffs() class CombinedPage(Page): form_model = 'player' form_fields = ['a'] + [f'bfirst_{i}' for i in range(1, 21)] + [f'bsecond_{i}' for i in range(1, 21)] def is_displayed(self): return self.round_number in [1, C.ROUND_SHOCK] def vars_for_template(self): return { 'round_number': self.round_number, } def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) form = context['form'] context['a_fields'] = [form['a']] context['bfirst_fields'] = [form[f'bfirst_{i}'] for i in range(1, 21)] context['bsecond_fields'] = [form[f'bsecond_{i}'] for i in range(1, 21)] return context @staticmethod def error_message(player, values): print('values is', values) total1 = sum((values.get(f'bfirst_{i}') or 0) for i in range(1, 21)) total2 = sum((values.get(f'bsecond_{i}') or 0) for i in range(1, 21)) errors = {} if total1 != 100: for i in range(1, 21): errors[f'bfirst_{i}'] = 'The numbers must add up to 100' if total2 != 100: for i in range(1, 21): errors[f'bsecond_{i}'] = 'The numbers must add up to 100' return errors class FinalPage(Page): def is_displayed(self): return self.round_number == C.NUM_ROUNDS def vars_for_template(player: Player): return { 'tot_payoff_guessing_2pg': player.tot_payoff_guessing_2pg, 'tot_payoff_first_2pg': player.tot_payoff_first_2pg, 'tot_payoff_second_2pg': player.tot_payoff_second_2pg, 'tot_payoff_2pg': player.tot_payoff_2pg, } class EndExperiment(WaitPage): def is_displayed(self): return self.round_number == C.NUM_ROUNDS def after_all_players_arrive(self): for player in self.group.get_players(): player.calculate_final_payoff() class EndResults(Page): def is_displayed(self): return self.round_number == C.NUM_ROUNDS def vars_for_template(self): return { 'final_payoff': self.final_payoff, 'final_payoff_in_euro': self.final_payoff_in_euro, 'payoff_plus_showup': self.payoff_plus_showup, 'part_one_payoff': self.participant.vars['survey'], #'part_two_payoff': self.participant.vars['one_player_gg'], 'part_three_payoff': self.participant.vars['two_player_gg'] } page_sequence = [#PartThreeBegins, #InstructionsPartThree, ShockOccurs, CombinedPage, GuessingGame, GuessingGameWaitPage, WaitForChoices, #Beliefs, #WaitForBeliefs, #BeliefsTwo, GroupResultsWaitPage, SessionResultsWaitPage, #ShowResults, RoundResults, WaitForFinalPage, #FinalPage, EndExperiment, EndResults ]