from otree.api import * c = cu doc = '' class Constants(BaseConstants): name_in_url = 'PGG_disaster' players_per_group = 6 num_rounds = 5 endowment = cu(120) multiplier = 2 interest_rate = 10 disaster_charts_template = 'PGG_disaster/disaster_charts.html' class Subsession(BaseSubsession): 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() def turn_setup(group): session = group.session subsession = group.subsession import random print('START OF TURN ', subsession.round_number) ## Get players players = group.get_players() ## Severity forecast (integer 1 to half of starting endowment) subsession.severity = random.randint(1, Constants.endowment / 2) print('severity for round', group.round_number, "=", subsession.severity) ## Hit probability for each player (1-99%) 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 round', group.round_number, "=", hit_probs) ## Set player resources for start of turn and record previous round for reporting #If turn 1, then initial endowment; else resource_end of previous turn if group.round_number == 1: for p in players: p.resource_start = Constants.endowment #player resources at start of turn p.leftover_damage_prev = 0 #report player leftover damage from previous turn p.contribution_prev = 0 #report player contribution from previous turn p.loss_prev = 0 #report player contribution from previous turn else: 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] 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 if group.round_number == 1: subsession.shield_0 = 0 #next turn shield subsession.total_contribution_prev = 0 #report previous total contributions subsession.severity_prev = 0 #report previous severity subsession.total_damage_prev = 0 #report previous damage subsession.shield_1_prev = 0 #report previous community shield subsession.leftover_damages_prev = 0 #report previous leftover damages else: subsession.shield_0 = subsession.in_round(group.round_number - 1).shield_2 subsession.total_contribution_prev = subsession.in_round(group.round_number - 1).total_contribution subsession.severity_prev = subsession.in_round(group.round_number - 1).severity subsession.total_damage_prev = subsession.in_round(group.round_number - 1).total_damage subsession.shield_1_prev = subsession.in_round(group.round_number - 1).shield_1 subsession.leftover_damages_prev = subsession.in_round(group.round_number - 1).leftover_damages print('shield 0', subsession.shield_0) def turn_results(group): session = group.session subsession = group.subsession 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 ###### BOT FOR DROPOUTS #################################################### ## RISK NEUTRAL STRATEGY - 1 TURN # calculate total expected NET DAMAGE of disaster based on the shield at the start of the turn hit_prob_bots = [p.hit_prob for p in players] exp_damage_bots = sum(hit_prob_bots) * subsession.severity exp_netdamage_bots = max(exp_damage_bots - subsession.shield_0, 0) print('hit_prob_bots ', hit_prob_bots) print('exp_damage_bots ', exp_damage_bots) print('exp_netdamage_bots ', exp_netdamage_bots) # Create bot contribution for dropouts for p in players: if p.flag_dropout_player == True: # calculate how much of expected net damage will be allocated to you exp_netdamage_bot = exp_netdamage_bots * (p.hit_prob / sum(hit_prob_bots) ) # take this amount and divide by multiplier for 1-turn risk-neutral strategy exp_contribute_bot = exp_netdamage_bot / Constants.multiplier # make sure can't contribute more than current resources exp_contribute_bot_2 = min(exp_contribute_bot, p.resource_start) # make sure can't contribute anything if have 0 or fewer points exp_contribute_bot_3 = max(0, exp_contribute_bot_2) # make contribution p.contribution = c(exp_contribute_bot_3) print(exp_netdamage_bot, exp_contribute_bot, exp_contribute_bot_2, exp_contribute_bot_3) ############################################################################ ## Tally up contributions from each player contributions = [p.contribution for p in players] print('individual contributions', contributions) ## Sum contributions subsession.total_contribution = sum(contributions) print('total contribution', subsession.total_contribution) ## Calculate shield in middle of turn (after donations but prior to damages) as shield_0 + contributions*multiplier subsession.shield_1 = subsession.shield_0 + (subsession.total_contribution * Constants.multiplier) print('shield 1', subsession.shield_1) ## Calculate damage for each player for p in players: #create array of rand-uniform draws and compare with hit prob rand_vect = [0] * subsession.severity hit_vect = [0] * subsession.severity for i in range(1, subsession.severity + 1): rand_vect[i-1] = random.uniform(0, 1) #if random number is within hit probability, suffer a hit 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) damages = [p.damage for p in players] subsession.total_damage = sum(damages) print('individual damage', damages) print('total damage', subsession.total_damage) ## Calculate shield at end of turn (after donations and damages) as max(0, shield_1 - total_damage) subsession.shield_2 = max(0, subsession.shield_1 - subsession.total_damage) print('shield 2', subsession.shield_2) ## If total_damage is greater than shield_1, we have left over damage leftover_total_damage = -1* min(0, subsession.shield_1 - subsession.total_damage) subsession.leftover_damages = leftover_total_damage print('leftover damage', leftover_total_damage) ## Redistribute any left over damage to players, weighted by player damage vector #if someone 0 damage from disaster, we need to safegaurd division by zero if subsession.total_damage >= 1: leftover_prob = [d / subsession.total_damage for d in damages] else: leftover_prob = [0 for d in damages] print(leftover_prob) #multiply relative hit probabilities by net damage for p in players: p.leftover_damage = leftover_prob[p.id_in_group - 1] * leftover_total_damage leftover_damages = [p.leftover_damage for p in players] print('leftover_damages', leftover_damages) ## Calculate total losses as sum of net damage plus amount donated for p in players: p.loss = p.contribution + p.leftover_damage losses = [p.loss for p in players] print('losses', losses) subsession.total_loss = sum(losses) print('total loss', subsession.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) resources_end = [p.resource_end for p in players] print('resources at end with interest', resources_end) resources_end_noint = [x /(1 + Constants.interest_rate / 100) for x in resources_end] print('resources at end before interest', resources_end_noint) class Group(BaseGroup): flag_dropout_group = models.BooleanField(initial=False) turn_setup = turn_setup turn_results = turn_results def contribution_max(player): 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) contribution_max = contribution_max class Wait_Group(WaitPage): group_by_arrival_time = True @staticmethod def is_displayed(player): session = player.session subsession = player.subsession return subsession.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): return dict( remaining_resource = player.resource_start / (1 + Constants.interest_rate / 100) ) @staticmethod def js_vars(player): session = player.session subsession = player.subsession group = player.group return dict( severity = subsession.severity, endowment_half = Constants.endowment / 2, hit_probs_others = [0] + [round(p.hit_prob * 100) for p in player.get_others_in_group()], hit_probs_you = [round(player.hit_prob * 100)] + [0]*(Constants.players_per_group - 1), categories = ['You'] + ['Player ' + str(i) for i in range(2, Constants.players_per_group + 1)] ) @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): session = player.session subsession = player.subsession return (subsession.round_number == Constants.num_rounds and player.flag_dropout_player == True) class Completion(Page): form_model = 'player' @staticmethod def is_displayed(player): session = player.session subsession = player.subsession return (subsession.round_number == Constants.num_rounds and player.flag_dropout_player == False) page_sequence = [Wait_Group, Wait_Setup, Play, Wait_Results, Dropout, Completion]