from otree.api import * c = cu doc = '' class Constants(BaseConstants): name_in_url = 'PGG_disaster_single' players_per_group = None num_rounds = 5 endowment = cu(100) multiplier = 2 interest_rate = 0 num_computers = 2 initial_shield = cu(0) bot_standard_deviation = 1 disaster_charts_template = 'PGG_disaster_single/disaster_charts.html' def group_by_arrival_time_method(subsession, waiting_players): session = subsession.session print('in subsession group_by_arrival_time_method') # get list of waiting players pp = [p for p in waiting_players] # single player --> group into 1 player print('about to create a group') return [pp[0]] class Subsession(BaseSubsession): group_by_arrival_time_method = group_by_arrival_time_method def turn_setup(group): import random print('in group turn_setup method') print('START OF TURN ', group.round_number) ## Get players players = group.get_players() print(players) ## Set severity (1 to half of starting endowment) group.severity = random.randint(1, Constants.endowment / 2) print('severity for round', group.round_number, "=", group.severity) ## Hit probability for players for p in players: p.hit_prob = random.uniform(0.005, 0.994) p.hit_prob_disp = '{:.0%}'.format(p.hit_prob) hit_probs = [p.hit_prob_disp for p in players] print('hit probs for you round', group.round_number, "=", hit_probs) ## Hit probability for others (bots) group.bot1_hit = random.uniform(0.005, 0.994) group.bot2_hit = random.uniform(0.005, 0.994) hit_others = [group.bot1_hit, group.bot2_hit] print('hit prob for others round', group.round_number, "=", hit_others) ## Set player resources for start of turn and record previous round for reporting if group.round_number == 1: group.bot1_resource_start = Constants.endowment - group.bot1_pre_contribute group.bot2_resource_start = Constants.endowment - group.bot2_pre_contribute for p in players: p.resource_start = Constants.endowment - p.pre_contribute #starting resources p.leftover_damage_prev = 0 #leftover damage from previous turn (for reporting) p.contribution_prev = 0 #player contribution from previous turn (for reporting) p.loss_prev = 0 #player loss from previous turn (for reporting) else: group.bot1_resource_start = group.in_round(group.round_number - 1).bot1_resource_end group.bot2_resource_start = group.in_round(group.round_number - 1).bot2_resource_end for p in players: p.resource_start = p.in_round(group.round_number - 1).resource_end p.leftover_damage_prev = p.in_round(group.round_number - 1).leftover_damage p.contribution_prev = p.in_round(group.round_number - 1).contribution p.loss_prev = p.in_round(group.round_number - 1).loss resources_start = [p.resource_start for p in players] + [group.bot1_resource_start] + [group.bot2_resource_start] print('resources at start of round', group.round_number, "=", resources_start) ## Set shield at start of turn (prior to donations and damages) and record previous round info for reporting; if turn 1, then 0, else shield_2 of previous turn #Get pre-game contributions if group.round_number == 1: pre_contributions = [p.pre_contribute for p in players] + [group.bot1_pre_contribute] + [group.bot2_pre_contribute] else: pre_contributions = [0] group.total_pre_contribution = sum(pre_contributions) #print('pregame contributions = ', pre_contributions) #print('sum pregame contributions = ', group.total_pre_contribution) if group.round_number == 1: group.shield_0 = 0 + 2 * (group.total_pre_contribution) group.total_contribution_prev = 0 #previous total contributions (for reporting) group.severity_prev = 0 #previous severity (for reporting) group.total_damage_prev = 0 #previous damage (for reporting) group.shield_0_prev = 0 #previous starting community shield (for reporting) group.shield_1_prev = 0 #previous total community shield (for reporting) group.leftover_damages_prev = 0 #previous leftover damages (for reporting) for p in players: p.damage_prev = 0 #previous expected damage to players (for reporting) else: group.shield_0 = group.in_round(group.round_number - 1).shield_2 group.total_contribution_prev = group.in_round(group.round_number - 1).total_contribution group.severity_prev = group.in_round(group.round_number - 1).severity group.total_damage_prev = group.in_round(group.round_number - 1).total_damage group.shield_0_prev = group.in_round(group.round_number - 1).shield_0 group.shield_1_prev = group.in_round(group.round_number - 1).shield_1 group.leftover_damages_prev = group.in_round(group.round_number - 1).leftover_damages for p in players: p.damage_prev = p.in_round(group.round_number - 1).damage print('shield_0 round', group.round_number, '=', group.shield_0) def turn_results(group): print('in group turn_results method') import random ## Get players players = group.get_players() ## Identify partcipants with dropout flag for p in players: if p.participant.vars.get('dropout'): #flag player dropout p.flag_dropout_player = True #flag dropout was present in the group group.flag_dropout_group = True ## If dropout, set contributions to 0 (will skip through game) for p in players: if p.flag_dropout_player == True: p.contribution = 0 ############################################################################################### ## BOT BEHAVIOR ############################################################################################### ## BOT1 is optimal (selfishly - if everyone looks after their own interests as best they can) ## BOT2 is optimal (communally - if everyone contributes equally to get an optimal shield value) print('inside bot strategies') # calculate total expected damage of disaster based on the shield at the start of the turn hit_probs = [p.hit_prob for p in players] + [group.bot1_hit] + [group.bot2_hit] exp_damage = sum(hit_probs) * group.severity exp_netdamage = max(exp_damage - group.shield_0, 0) print('hit_probs round', group.round_number, "=", hit_probs) print('exp_total_damage_bots round', group.round_number, "=", exp_damage) print('exp_total_netdamage_bots round', group.round_number, "=", exp_netdamage) exp_netdamage_bot1 = exp_netdamage * (group.bot1_hit / sum(hit_probs)) exp_netdamage_bot2 = exp_netdamage * (group.bot2_hit / sum(hit_probs)) exp_netdamage_bots = [exp_netdamage_bot1, exp_netdamage_bot2] print('exp_netdamage_bots round', group.round_number, "=", exp_netdamage_bots) #set standard deviation for noise (points) sd = Constants.bot_standard_deviation ## BOT1 strategy: selfishly optimal (with some noise) - contribues the expected net damage to ITSElF (based on starting shield) and divides this amount by the multiplier (with some noise) exp_contribute_bot1 = exp_netdamage_bot1 / Constants.multiplier #expected contribution exp_contribute_bot1_noise = round(random.gauss(exp_contribute_bot1, sd)) #add noise and round exp_contribute_bot1_max = min(exp_contribute_bot1_noise, group.bot1_resource_start) #restrict to available resources exp_contribute_bot1_min = max(0, exp_contribute_bot1_max) #can't contribute negative print('BOT1 contributios round', group.round_number, "=", exp_contribute_bot1_min) ## BOT2 strategy: communally optimal (with some noise) - contributes the expected net damage to the COMMUNITY (based on the starting shield) and divides this amount by the community size and the multiplier (with some noise) exp_contribute_bot2 = exp_netdamage / (Constants.multiplier * len(hit_probs)) #expected contribution exp_contribute_bot2_noise = round(random.gauss(exp_contribute_bot2, sd)) #add noise and round exp_contribute_bot2_max = min(exp_contribute_bot2_noise, group.bot2_resource_start) #restrict to available resources exp_contribute_bot2_min = max(0, exp_contribute_bot2_max) #can't contribute negative print('BOT2 contributios round', group.round_number, "=", exp_contribute_bot2_min) #change final contributions to currency data type group.bot1_contribute = c(exp_contribute_bot1_min) group.bot2_contribute = c(exp_contribute_bot2_min) bot_contributions = [group.bot1_contribute, group.bot2_contribute] print('bot contributions round', group.round_number, "=", bot_contributions) ################################################################################################## ## Get contributions from each player contributions = [p.contribution for p in players] print('player contributions round', group.round_number, "=", contributions) ## Sum contributions (player and bots) group.total_contribution = sum(contributions) + sum(bot_contributions) print('total contribution round', group.round_number, "=", group.total_contribution) ## Calculate shield in middle of turn (after donations but prior to damages) as shield_0 + contributions*multiplier group.shield_1 = group.shield_0 + (group.total_contribution * Constants.multiplier) print('shield_1 round', group.round_number, "=", group.shield_1) ## Calculate initial damage for each player (and bots) for p in players: #create array of random uniform draws and compare it with hit probabilities; if within range, suffer hit rand_vect = [0] * group.severity hit_vect = [0] * group.severity for i in range(1, group.severity + 1): rand_vect[i-1] = random.uniform(0, 1) if rand_vect[i-1] <= p.hit_prob: hit_vect[i-1] = 1 p.damage = sum(hit_vect) #print('rand_vect', rand_vect) #print('hit_vect', hit_vect) rand_vect_bot1 = [0] * group.severity hit_vect_bot1 = [0] * group.severity rand_vect_bot2 = [0] * group.severity hit_vect_bot2 = [0] * group.severity for j in range(1, group.severity + 1): rand_vect_bot1[j-1] = random.uniform(0, 1) rand_vect_bot2[j-1] = random.uniform(0, 1) if rand_vect_bot1[j-1] <= group.bot1_hit: hit_vect_bot1[j-1] = 1 if rand_vect_bot2[j-1] <= group.bot2_hit: hit_vect_bot2[j-1] = 1 bot1_damage = sum(hit_vect_bot1) bot2_damage = sum(hit_vect_bot2) damages = [p.damage for p in players] + [bot1_damage] + [bot2_damage] group.total_damage = sum(damages) print('damages round', group.round_number, "=", damages) print('total damage round', group.round_number, "=", group.total_damage) ############################################################################################################ ### Interaction with shield and net damages (choose Method 1 or 2) ############################################################################################################ #### Method 1: Divide Shield up Equally and apply to individual damages ### Divide shield up equally (rounding) #shield_divide = round(group.shield_1 / (len(hit_probs))) #print('shield divide round ', group.round_number, "=", shield_divide) ### Subtract shield from each player (bot) damage #for p in players: # p.leftover_damage = max(p.damage - shield_divide, 0) # p.shield_remain = -min(p.damage - shield_divide, 0) #bot1_leftover_damage = max(bot1_damage - shield_divide, 0) #bot1_shield_remain = -min(bot1_damage - shield_divide, 0) #bot2_leftover_damage = max(bot2_damage - shield_divide, 0) #bot2_shield_remain = -min(bot2_damage - shield_divide, 0) #leftover_damage = [p.leftover_damage for p in players] + [bot1_leftover_damage] + [bot2_leftover_damage] #print('leftover damages round', group.round_number, "=", leftover_damage) #group.leftover_damages = sum(leftover_damage) ### Carry over any remaining shield #shields_remain = [p.shield_remain for p in players] + [bot1_shield_remain] + [bot2_shield_remain] #print('shields remain round', group.round_number, "=", shields_remain) #group.shield_2 = sum(shields_remain) #print('shield_2 round', group.round_number, "=", group.shield_2) ### Method 2: Subtract total shield from total damage and then redistribute remaining damage ## Calculate shield at end of turn (after donations and damages) as max(0, shield_1 - total_damage) group.shield_2 = max(0, group.shield_1 - group.total_damage) print('shield_2 round', group.round_number, "=", group.shield_2) ## If total_damage is greater than shield_1, we have leftover damage group.leftover_damages = -1* min(0, group.shield_1 - group.total_damage) print('leftover total damage round', group.round_number, "=", group.leftover_damages) ## Redistribute any left over damage to players, weighted by player damage vector; if there is 0 total damage from the disaster, we need to safegaurd its division by zero in the denominator if group.total_damage >= 1: leftover_prob = [(d / group.total_damage) for d in damages] else: leftover_prob = [0 for d in damages] print('leftover probabilities round', group.round_number, "=", leftover_prob) #multiply relative leftover probabilities by net damage for p in players: p.leftover_damage = leftover_prob[p.id_in_group - 1] * group.leftover_damages bot1_leftover_damage = leftover_prob[-(Constants.num_computers)] * group.leftover_damages bot2_leftover_damage = leftover_prob[-(Constants.num_computers) + 1] * group.leftover_damages leftover_damages = [p.leftover_damage for p in players] + [bot1_leftover_damage] + [bot2_leftover_damage] print('leftover damages round', group.round_number, "=", leftover_damages) print('check to make sure equals starting leftover damage =', group.leftover_damages) ############################################################################################################# ############################################################################################################# ## Calculate total losses as sum of net damage plus amount donated for p in players: p.loss = p.contribution + p.leftover_damage bot1_loss = group.bot1_contribute + bot1_leftover_damage bot2_loss = group.bot2_contribute + bot2_leftover_damage losses = [p.loss for p in players] + [bot1_loss] + [bot2_loss] print('losses round', group.round_number, "=", losses) group.total_loss = sum(losses) print('total loss round', group.round_number, "=", group.total_loss) ## Calculate player resources at end of turn (resources at start - total losses) * interest_rate #NOTE: This number can go below 0, viewed thematically as being on welfare. However, players cannot donate anything if they have negative resources. for p in players: p.resource_end = (p.resource_start - p.loss) * (1 + Constants.interest_rate / 100) group.bot1_resource_end = (group.bot1_resource_start - bot1_loss) * (1 + Constants.interest_rate / 100) group.bot2_resource_end = (group.bot2_resource_start - bot2_loss) * (1 + Constants.interest_rate / 100) resources_end = [p.resource_end for p in players] + [group.bot1_resource_end] + [group.bot2_resource_end] resources_end_noint = [x /(1 + Constants.interest_rate / 100) for x in resources_end] print('resources at end before interest round', group.round_number, "=", resources_end_noint) print('resources at end with interest round', group.round_number, "=", resources_end) def pre_contribute(group): print('in group pre_contribute method') import random ## Get players players = group.get_players() ## Identify partcipants with dropout flag for p in players: if p.participant.vars.get('dropout'): #flag player dropout p.flag_dropout_player = True #flag dropout was present in the group group.flag_dropout_group = True ## If dropout, set pre game contribution to 0 for p in players: if p.flag_dropout_player == True: p.pre_contribute = 0 ############################################################################################### ## BOT BEHAVIOR ############################################################################################### #set standard deviation for noise (points) sd = Constants.bot_standard_deviation #The expected damage of a disaster with random severity (1-50) and random chance of being hit (1-100%) is equal to (50+1)/2 * 0.5, which equals 12.75. Divide this by the multiplier to get optimal individual or communal strategy (both yield the same answer here) group.bot1_pre_contribute = round(random.gauss(12.75 / Constants.multiplier, sd)) group.bot2_pre_contribute = round(random.gauss(12.75 / Constants.multiplier, sd)) bot_pre_contributions = [group.bot1_pre_contribute, group.bot2_pre_contribute] print('bot pregame contributions = ', bot_pre_contributions) ################################################################################################## class Group(BaseGroup): flag_dropout_group = models.BooleanField(initial=False) total_contribution = models.CurrencyField() total_damage = models.CurrencyField() total_loss = models.CurrencyField() severity = models.IntegerField() shield_0 = models.CurrencyField() shield_1 = models.CurrencyField() shield_2 = models.CurrencyField() total_contribution_prev = models.CurrencyField() severity_prev = models.IntegerField() total_damage_prev = models.CurrencyField() shield_1_prev = models.CurrencyField() leftover_damages = models.CurrencyField() leftover_damages_prev = models.CurrencyField() bot1_hit = models.FloatField() bot2_hit = models.FloatField() bot1_resource_start = models.CurrencyField() bot2_resource_start = models.CurrencyField() bot1_contribute = models.CurrencyField() bot2_contribute = models.CurrencyField() bot1_resource_end = models.CurrencyField() bot2_resource_end = models.CurrencyField() bot1_pre_contribute = models.CurrencyField() bot2_pre_contribute = models.CurrencyField() total_pre_contribution = models.CurrencyField() shield_0_prev = models.CurrencyField() turn_setup = turn_setup turn_results = turn_results pre_contribute = pre_contribute def contribution_max(player): # Can't contribute negative if you have negative resources return player.resource_start if player.resource_start >= 0 else 0 class Player(BasePlayer): contribution = models.CurrencyField(label='How many resources will you contribute to the community shield?', min=0) damage = models.CurrencyField(min=0) loss = models.CurrencyField(min=0) hit_prob = models.FloatField(max=1, min=0) hit_prob_disp = models.StringField() leftover_damage = models.CurrencyField() resource_start = models.CurrencyField() resource_end = models.CurrencyField() leftover_damage_prev = models.CurrencyField() contribution_prev = models.CurrencyField() loss_prev = models.CurrencyField() flag_dropout_player = models.BooleanField(initial=False) shield_remain = models.IntegerField() damage_prev = models.CurrencyField() pre_contribute = models.CurrencyField(label='Would you like to contribute any of your points to the community shield before the first turn? If so, how many?', max=Constants.endowment, min=0) feedback = models.LongStringField(initial='...', label='If you have any comments or feedback on how we can improve this game, please let us know:') contribution_max = contribution_max class Wait_Group(WaitPage): group_by_arrival_time = True @staticmethod def is_displayed(player): group = player.group return group.round_number == 1 class Pre_Contribution(Page): form_model = 'player' form_fields = ['pre_contribute'] timeout_seconds = 180 @staticmethod def is_displayed(player): group = player.group return group.round_number == 1 @staticmethod def before_next_page(player, timeout_happened): participant = player.participant if timeout_happened: participant.vars['dropout'] = True class Wait_Pre_Contribute(WaitPage): after_all_players_arrive = 'pre_contribute' @staticmethod def is_displayed(player): group = player.group return group.round_number == 1 class Wait_Setup(WaitPage): after_all_players_arrive = 'turn_setup' class Play(Page): form_model = 'player' form_fields = ['contribution'] timeout_seconds = 180 @staticmethod def is_displayed(player): participant = player.participant # Show page if partcipants have not dropped out return not participant.vars.get('dropout') @staticmethod def vars_for_template(player): group = player.group return dict( remaining_resource = player.resource_start / (1 + Constants.interest_rate / 100), group_loss = group.leftover_damages_prev + group.total_contribution_prev, total_add = group.total_contribution_prev * 2, shield_you = round(group.shield_1_prev / (Constants.num_computers + 1)) ) @staticmethod def js_vars(player): group = player.group return dict( severity = group.severity, endowment_half = Constants.endowment / 2, hit_probs_others = [0] + [round(group.bot1_hit * 100)] + [round(group.bot2_hit * 100)], hit_probs_you = [round(player.hit_prob * 100)] + [0]*(Constants.num_computers), resources_others = [0] + [group.bot1_resource_start] + [group.bot2_resource_start], resources_you = [player.resource_start] + [0]*(Constants.num_computers), categories = ['You'] + [('Player ' + str(i)) for i in range(2, Constants.num_computers + 2)] ) @staticmethod def before_next_page(player, timeout_happened): participant = player.participant if timeout_happened: participant.vars['dropout'] = True class Wait_Results(WaitPage): after_all_players_arrive = 'turn_results' class Dropout(Page): form_model = 'player' @staticmethod def is_displayed(player): group = player.group return (group.round_number == Constants.num_rounds and group.flag_dropout_group == True) class Completion(Page): form_model = 'player' form_fields = ['feedback'] @staticmethod def is_displayed(player): group = player.group return (group.round_number == Constants.num_rounds and group.flag_dropout_group == False) page_sequence = [Wait_Group, Pre_Contribution, Wait_Pre_Contribute, Wait_Setup, Play, Wait_Results, Dropout, Completion]