from otree.api import * import random import json doc = """ Your app description """ #GLOBAL VARIABLES #current_corruption_level = 0 corruption_levels = [1, 2, 3] corruption_levels_dict = { 3: ('high', 0.8), #high corruption rate implies 80% corruption incidence. 2: ('medium', 0.4), #medium corruption rate implies 50% corruption incidence. 1: ('low', 0.2), #low corruption rate implies 20% corruption incidence. } #MODELS class C(BaseConstants): NAME_IN_URL = 'corruption' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3 EXTERNALITY_AMOUNT = 3 MAX_PUNISHMENT = 42 OFFER_ROLE = 'Proposer' RESPONDER_ROLE = 'Responder' OFFER_ACCEPT = 85 OFFER_REJECT = 60 DONTOFFER_REJECT = 60 class Subsession(BaseSubsession): ''' Matches is a dictionary whith a key representing a player and the elements being a list of the player's matches. Payoffs is the dictionary showing each play's payoff. The keys are players and the elements are payoffs. The payoff list contains two entries, the first is the payoff when a player's role is Offer and the second is his payoff when his role is Responder Because these fields are dictionaries, I change them to strings using the json module. ''' matches = models.StringField() payoffs = models.StringField() level = models.StringField() class Group(BaseGroup): pass class Player(BasePlayer): ''' Unlike the bribe game, I put the offer and response fields in the player model because the matches do not tie the players to each other. ''' offer = models.BooleanField( choices = [ [True, 'Offer'], [False, 'Don\'t Offer'], ], widget = widgets.RadioSelect, label='Will you collude with your partner?' ) response = models.BooleanField( choices = [ [True, 'Accept'], [False, 'Reject'], ], widget = widgets.RadioSelect, label='Do you accept the offer?' ) matches = models.StringField() #players that the player is matched with. A player can be matched with more #than one player so that the likelihood of meeting a certain percentage of players is realised payoffs = models.StringField() tokens_for_offered = models.IntegerField( min=0, max=10, initial=0, label='My partner made an offer') tokens_for_notoffered = models.IntegerField( min=0, max=10, initial=0, label='My partner did not make an offer') token_payoff = models.StringField() #FUNCTIONS def get_corrupt_and_clean_players(players): ''' This function returns clean and corrupt participant depending on whether they offered a bribe in the previous round ''' clean_players = [a_player for a_player in players if a_player.participant.offer == True] corrupt_players = [a_player for a_player in players if a_player.participant.offer == False] return [clean_players, corrupt_players] def get_matches(matches): ''' This function summarizes the matching done by the function match_players. I creates pairwise matches in a tuple. It can be hard to inspect player object during testing, so I use player's id's to do my matches. This then involves moving back and forth from id's, which are integers, to player objects. ''' match_pairs = [] for player_id in matches.keys(): if matches[player_id]: for other_player_id in matches[player_id]: match_pairs.append((player_id, other_player_id)) if player_id in matches[other_player_id]: matches[other_player_id].remove(player_id) return match_pairs def match_players(level, clean_players, corrupt_players): ''' The function matches players in pairs. Two decisions are made per round. In the first decision, a player decides to convince the other to collude with him. In the second, the player is responding to the request of the other to collude with him. I start by creating a group from which I draw potential partners. That group is made up of a certain proportion of corrupt players to all players. For example, 80% corruption rate means that a player has 80% chance to meet with a player who offered to collude in the first round. This I do by sampling with replacement. That group I call group_of_clean_and_corrupt players. I select a match from this group, everytime I fail to get a match, I resample the same proportions from the population untill I get the matches. ''' players = corrupt_players + clean_players matches = {player.id_in_group:[] for player in players} #if all players are clean or are dirty, transfer one player to the other category so the game continues. if len(corrupt_players) == 0: corrupt_players.append(clean_players.pop()) if len(corrupt_players) == len(players): clean_players.append(corrupt_players.pop()) len_players = len(players) len_corrupt_players = len(corrupt_players) len_clean_players = len(clean_players) num_corrupt_players_per_level = int(len_players * corruption_levels_dict[level][1]) if len_corrupt_players < num_corrupt_players_per_level : num_corrupt_players_per_level = len_corrupt_players num_clean_players_per_level = int(num_corrupt_players_per_level / corruption_levels_dict[level][1]) \ - num_corrupt_players_per_level elif len_corrupt_players > num_corrupt_players_per_level: num_clean_players_per_level = len_clean_players num_corrupt_players_per_level = int(num_clean_players_per_level / (1 - corruption_levels_dict[level][1])) \ - num_clean_players_per_level else: num_clean_players_per_level = len_players - num_corrupt_players_per_level for player in players: if not matches[player.id_in_group]: while True: group_of_clean_and_corrupt_players = random.choices(corrupt_players, k=num_corrupt_players_per_level) \ + random.choices(clean_players, k= num_clean_players_per_level) a_match = random.choice(group_of_clean_and_corrupt_players) if a_match != player: matches[player.id_in_group].append(a_match.id_in_group) matches[a_match.id_in_group].append(player.id_in_group) break return matches def set_payoffs(players, matches): ''' I set the payoffs by considering both decisions: an fffer decision and a response decision. For each pair, I check how they offered and how their partners responded. I then reverse the same decisions in round two. If I have a true offer and true response decision, I decrease the payoff of the others by R3 ''' payoffs = {player.id_in_group:{} for player in players} id_to_player_dict = {player.id_in_group: player for player in players} payoff_dict = get_payoffs_dict() for player_id, other_player_id in matches: payoffs[player_id][other_player_id] = [0, 0] payoffs[other_player_id][player_id] = [0, 0] for a_round in range(2): for player_id, other_player_id in matches: player = id_to_player_dict[player_id] other_player = id_to_player_dict[other_player_id] if a_round == 0: payoffs[player_id][other_player_id][a_round] += payoff_dict[(player.offer, other_player.response)] payoffs[other_player_id][player_id][a_round] += payoff_dict[(player.offer, other_player.response)] if player.offer and other_player.response: payoffs = reduce_payoffs(payoffs, player_id, other_player_id, matches, a_round) if a_round == 1: payoffs[player_id][other_player_id][a_round] += payoff_dict[(other_player.offer, player.response)] payoffs[other_player_id][player_id][a_round] += payoff_dict[(other_player.offer, player.response)] if player.response and other_player.offer: payoffs = reduce_payoffs(payoffs, player_id, other_player_id, matches, a_round) return payoffs def set_corruption_game_payoffs(players): ''' I set the participant field corruption_game_payoffs so that it can persist throughout all the apps. I store payoffs as string objects, but they are lists. So,I changed from and to strings by using a json module. ''' for a_player in players: payoffs_ls = [] for a_round in [1, 2, 3]: payoffs_in_a_round = json.loads(a_player.in_round(a_round).payoffs) payoffs_in_a_round_ls = [] for key in payoffs_in_a_round: payoffs_in_a_round_ls.append(payoffs_in_a_round[key]) payoffs_ls.append(payoffs_in_a_round_ls) a_player.participant.corruption_game_payoffs = payoffs_ls def reduce_payoffs(payoffs, curr_player_id, other_player_id, matches, a_round): ''' After I have found that corruption occured, that is, offer and response are both true, then I reduce all other player's payoffs except the one of the pair committing corruption. ''' externality_amount = min(C.EXTERNALITY_AMOUNT, int(C.MAX_PUNISHMENT / (len(matches) - 1))) #I vary the externality amount depending on the number of matches, otherwise, payoff becomes negative quickly for player_id in payoffs.keys(): for a_match_id in payoffs[player_id].keys(): if player_id != curr_player_id or a_match_id != other_player_id: payoffs[player_id][a_match_id][a_round] -= externality_amount return payoffs def set_tokens_payoff(subsession, matches, players): ''' Tokens payoffs rewards correct guesses a player makes about making an offer to collude or not. Suppose that a player's partner made an offer to collude, and that the player put 8 tokens to partner_offered, then the player's payoff is 16ZAR ''' id_to_player_dict = {a_player.id_in_group: a_player for a_player in players} token_payoffs_dict = {a_player.id_in_group: [] for a_player in players} for a_player_id, other_player_id in matches: a_player = id_to_player_dict[a_player_id] other_player = id_to_player_dict[other_player_id] if other_player.offer: token_payoffs_dict[a_player_id].append(2 * other_player.tokens_for_offered) else: token_payoffs_dict[a_player_id].append(2 * other_player.tokens_for_notoffered) if a_player.offer: token_payoffs_dict[other_player_id].append(2 * a_player.tokens_for_offered) else: token_payoffs_dict[other_player_id].append(2 * a_player.tokens_for_notoffered) a_player.token_payoff = json.dumps(token_payoffs_dict[a_player_id]) other_player.token_payoff = json.dumps(token_payoffs_dict[other_player_id]) def set_corruption_game_token_payoff(players): ''' I set the participant field corruption_game_token_payoff so it persist throughout the apps ''' for a_player in players: corruption_game_token_payoffs = [] for a_round in [1, 2, 3]: corruption_game_token_payoffs.append(json.loads(a_player.in_round(a_round).token_payoff)) a_player.participant.corruption_game_token_payoff = corruption_game_token_payoffs def get_payoffs_dict(): ''' A payoff dictionary which is read as follows: If a proposer has offered a bribe (True) and a responder has accepted a bribe (True), then a player receive R75. If a proposer has offered a bribe (True) and a responder has rejected a bribe (False), then a player receives R50 If a proposer has not offered a bribe (False) and a responder has accepted a bribe (True), ;), then a player receive R50 If a proposer has not offered a bribe (False) and a responder has rejected a bribe (False), then a player receive R50 ''' return { (True, True): C.OFFER_ACCEPT, (True, False): C.OFFER_REJECT, (False, True): C.DONTOFFER_REJECT, (False, False): C.DONTOFFER_REJECT } # PAGES class GroupWaitPage(WaitPage): wait_for_all_groups = True @staticmethod def after_all_players_arrive(subsession: Subsession): ''' I shuffle the players in this wait page. The shuffling logic cannot be done in creating_session because it depends on what happens in the first part of the game. ''' level_ls = [2, 3, 1] levels_subsession_map = {1:0, 2:1, 3:2} # First randomization simply by doing 80% likelihood of meeting corrupt participants, 20%, and then 50%. #Second randomization is 20%, 30%, and 40% level = level_ls[levels_subsession_map[subsession.round_number]] subsession.level = str(level) players = subsession.get_players() clean_players, corrupt_players = get_corrupt_and_clean_players(players) matches_dict = match_players(level, clean_players, corrupt_players) matches = get_matches(matches_dict) matches_str = json.dumps(matches) subsession.matches = matches_str class Offer(Page): form_model = 'player' form_fields = ['offer'] @staticmethod def vars_for_template(player: Player): level = int(player.subsession.level) return dict(role=C.OFFER_ROLE, level=level) class Response(Page): form_model = 'player' form_fields = ['response'] @staticmethod def vars_for_template(player: Player): level = int(player.subsession.level) return dict(role=C.RESPONDER_ROLE, level=level) class ResultsWaitPage(WaitPage): wait_for_all_groups = True @staticmethod def after_all_players_arrive(subsession: Subsession): ''' I set payoff by calling the set_payoffs method. I then store each player's payoff in his payoffs field. U use the json module to change backand forth from the string and lists. ''' players = subsession.get_players() matches_str = subsession.matches matches = json.loads(matches_str) payoffs = set_payoffs(players, matches) for player in players: participant = player.participant player_payoffs = payoffs[player.id_in_group] player_payoffs_str = json.dumps(player_payoffs) player.payoffs = player_payoffs_str set_tokens_payoff(subsession, matches, players) if subsession.round_number == 3: set_corruption_game_token_payoff(players) set_corruption_game_payoffs(players) class Beliefs(Page): form_model = 'player' form_fields = ['tokens_for_offered', 'tokens_for_notoffered'] @staticmethod def error_message(player, values): if values['tokens_for_offered'] + values['tokens_for_notoffered'] != 10: return 'The tokens should sum up to 10' page_sequence = [GroupWaitPage, Offer, Response, Beliefs, ResultsWaitPage ]