from otree.api import * import random from django import forms doc = """ In our experiment participants play the role of firms choosing whether or not to form a cartel. Cartels are illegal, and are fined if discovered by a competition authority. Treatments vary in the design of these fines.

This is the app which contains the main game taking place in the experiment.

18 participants take part in each session, which consists of three supergames of predetermined length. Ahead of each supergame, participants are matched in groups of three within two matching-pools of 9.

Here I have combined three apps to make repetition easier. First, we allow firms the choice whether or not to join a cartel. Then, we allow firms to independently decide on prices. Finally, the cartels are told whether or not they were discovered and fined.

In the voting app:
We allow firms the choice whether or not to join a cartel. If a cartel is formed, a chat window is formed allowing for free chat during the decision period.

In the price-setting app:
Firms simultaneously decide what price to set. Revenue, costs, and profits are determined.

In the antitrust app:
Cartels are informed whether they were discovered. Fines are calculated and subtracted from earnings. """ def cumsum(lst): total = 0 new = [] for ele in lst: total += ele new.append(total) return new class C(BaseConstants): NAME_IN_URL = 'vote_bertrand' # First supergame lasts x rounds, second supergame lasts y, etc... ROUNDS_PER_SG = [2, 2, 2] SG_ENDS = cumsum(ROUNDS_PER_SG) NUM_ROUNDS = sum(ROUNDS_PER_SG) # Informational texts INFORMATION_TEMPLATE = __name__ + '\instructions_comm.html' CARTEL_INFO = __name__ + '\cartel_info.html' INSTRUCTIONS_TEMPLATE = __name__ + '\instructions_price.html' # Production capacity to guarantee positive demand MAX_DEMAND = 100 PLAYERS_PER_GROUP = 3 # Constant marginal cost of production across all treatments MARGINAL_COST = 20 # Define the Nash equilibrium for the Overcharge treatment NASH_PRICE = MARGINAL_COST NASH_QUANTITY = MAX_DEMAND - NASH_PRICE # Penalty rates for each treatment PROFIT_RATE = 1.2 REVENUE_RATE = 0.8 OVERCHARGE_RATE = 0.5 class Subsession(BaseSubsession): sg = models.IntegerField() period = models.IntegerField() is_last_period = models.BooleanField() # This is from the Supergames app def creating_session(subsession: Subsession): if subsession.round_number == 1: sg = 1 period = 1 # loop over all subsessions for ss in subsession.in_rounds(1, C.NUM_ROUNDS): ss.sg = sg ss.period = period # 'in' gives you a bool. for example: 5 in [1, 5, 6] # => True is_last_period = ss.round_number in C.SG_ENDS ss.is_last_period = is_last_period if is_last_period: sg += 1 period = 1 else: period += 1 # GROUPS class Group(BaseGroup): # Cartel formation yes_votes_cast = models.IntegerField(initial=0) votes_needed = models.IntegerField(default=C.PLAYERS_PER_GROUP, field_maybe_none=False) cartel_formed = models.BooleanField(initial=True) # Bertrand-game outcomes unit_price = models.CurrencyField(initial=0, doc="""Lowest price this round""") total_units = models.IntegerField(initial=0, doc="""Total units sold this round""") # Competition authority cartel_discovered = models.BooleanField(initial=False) @property def prev_round_group(group): return group.in_round(group.round_number - 1) def unit_price(group): players = group.get_players() return min([p.price for p in players]) def total_units(group): return C.MAX_DEMAND - group.unit_price() def set_units(group): players = group.get_players() lowest_price = group.unit_price() num_players_with_lowest_price = sum(p.price == lowest_price for p in players) if num_players_with_lowest_price > 0: units_per_player = group.total_units() / num_players_with_lowest_price else: units_per_player = 0 for p in players: if p.price == lowest_price: p.units = units_per_player else: p.units = 0 def set_revenue(group): players = group.get_players() for p in players: p.round_revenue = p.units * p.price def set_cost(group): players = group.get_players() for p in players: p.round_cost = C.MARGINAL_COST * p.units def set_no_fine_payoffs(group): players = group.get_players() for p in players: p.no_fine_payoff = (p.price - C.MARGINAL_COST) * p.units def check_if_cartel_discovered(group): if not group.cartel_formed: return False group.cartel_discovered = random.random() < 0.05 ########### TREATMENTS ################ # Define the fine for being in a cartel def set_cartel_fines(group): for p in group.get_players(): mode = group.session.config['mode'] if mode == "revenue": p.cartel_fine = C.REVENUE_RATE * p.round_revenue elif mode == "profit": p.cartel_fine = C.PROFIT_RATE * p.no_fine_payoff elif mode == "overcharge": p.cartel_fine = C.OVERCHARGE_RATE * max(0, (p.price - C.NASH_PRICE) * C.NASH_QUANTITY) else: raise ValueError(f"Mode not recognized {mode}") def set_group_split(group): group_split = group % 2 return group_split def set_payoffs(group): players = group.get_players() for p in players: p.payoff = (p.price - C.MARGINAL_COST) * p.units - p.cartel_fine # PLAYERS class Player(BasePlayer): # Cartel decision join_cartel = models.BooleanField(widget=widgets.RadioSelect, choices=[ (True, 'Yes, I want to join the cartel'), (False, 'No, I do not want to join the cartel') ]) voted_cartel = models.BooleanField(initial=True) # Bertrand-game outcomes units = models.FloatField( initial=0, doc="""Units sold by this player in this round""", widget=forms.TextInput(attrs={'step': 'any'}), ) # Remove decimal point and trailing zeroes @property def formatted_units(player): return '{:.1f}'.format(player.units).rstrip('0').rstrip('.') round_revenue = models.CurrencyField(initial=0) round_cost = models.CurrencyField(initial=0) no_fine_payoff = models.CurrencyField(initial=0) # Competition authority cartel_fine = models.CurrencyField(initial=0) # Calculator calculator_chat_timeout_seconds = models.IntegerField(initial=0) calculator_me = models.IntegerField( min = 0, label = 'Me', ) calculator_r1 = models.IntegerField( min = 0, label = 'Rival 1', ) calculator_r2 = models.IntegerField( min = 0, label = 'Rival 2', ) @property def is_cartel_formed(player): return player.group.cartel_formed # Define the price decision price = models.IntegerField( min=0, doc="""Price set by this player""", label="What price will you set?", ) @property def prev_player(player): return player.in_round(player.round_number - 1) @property def other_players(player): return player.get_others_in_group() @property def running_payoff(player) -> Currency: return sum(( prev_self.payoff for prev_self in player.in_all_rounds() )) ### CHAT class Message(ExtraModel): group = models.Link(Group) sender = models.Link(Player) text = models.StringField() def to_dict(msg: Message): return dict(sender=msg.sender.id_in_group, text=msg.text) def custom_export(players): yield ['Session', 'Group', 'Participant Code', 'Participant ID' 'Round_number', 'Chat'] # 'filter' without any args returns everything msgs = Message.filter() for msg in msgs: player = msg.sender participant = player.participant group = player.group session = player.session yield [session.code, group.id, participant.code, participant.id_in_session, player.round_number, msg.text] ####### PAGES ####### class Introduction(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class MatchingWaitPage(WaitPage): wait_for_all_groups = True @staticmethod def after_all_players_arrive(subsession): # This is the rematching mechanism if subsession.period == 1: players = subsession.get_players() matrix = [] if len(players) == 3: matrix = [[1,2,3]] elif len(players) == 9: if subsession.sg == 1: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session <= 3], [player.participant.id_in_session for player in players if 4 <= player.participant.id_in_session <= 6], [player.participant.id_in_session for player in players if 7 <= player.participant.id_in_session <= 9], ] elif subsession.sg == 2: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session in [1, 4, 7]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [2, 5, 8]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [3, 6, 9]], ] elif subsession.sg == 3: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session in [1, 6, 8]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [2, 4, 9]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [3, 5, 7]], ] elif len(players) == 18: if subsession.sg == 1: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session <= 3], [player.participant.id_in_session for player in players if 4 <= player.participant.id_in_session <= 6], [player.participant.id_in_session for player in players if 7 <= player.participant.id_in_session <= 9], [player.participant.id_in_session for player in players if 10 <= player.participant.id_in_session <= 12], [player.participant.id_in_session for player in players if 13 <= player.participant.id_in_session <= 15], [player.participant.id_in_session for player in players if 16 <= player.participant.id_in_session <= 18], ] elif subsession.sg == 2: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session in [1, 4, 7]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [2, 5, 8]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [3, 6, 9]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [10, 13, 16]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [11, 14, 17]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [12, 15, 18]], ] elif subsession.sg == 3: matrix = [ [player.participant.id_in_session for player in players if player.participant.id_in_session in [1, 6, 8]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [2, 4, 9]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [3, 5, 7]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [10, 15, 17]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [11, 13, 18]], [player.participant.id_in_session for player in players if player.participant.id_in_session in [12, 14, 16]], ] subsession.set_group_matrix(matrix) class New_Supergame(Page): @staticmethod def is_displayed(player: Player): subsession = player.subsession return subsession.period == 1 class Cartel_Vote(Page): form_model = 'player' form_fields = ['join_cartel'] def before_next_page(player, timeout_happened): if player.join_cartel: player.voted_cartel = True player.group.yes_votes_cast += 1 else: player.voted_cartel = False player.group.cartel_formed = False if player.group.yes_votes_cast == len(player.group.get_players()): player.group.cartel_formed = True def is_displayed(player: Player): # Display the cartel vote page at the start of a new supergame if player.round_number == 1 or player.subsession.period == 1: return True # Don't display the cartel vote page if the player was in an undetected cartel in the previous round prev_group = player.group.prev_round_group did_not_form_cartel = not prev_group.cartel_formed cartel_was_discovered = prev_group.cartel_discovered and prev_group.cartel_formed if did_not_form_cartel or cartel_was_discovered: return True return False class ResultsWaitVote(WaitPage): body_text = "Waiting for the other participants to vote." def is_displayed(player: Player): if player.round_number == 1 or player.subsession.period == 1: return True prev_group = player.group.prev_round_group did_not_form_cartel = not prev_group.cartel_formed cartel_was_discovered = prev_group.cartel_discovered and prev_group.cartel_formed if did_not_form_cartel or cartel_was_discovered: return True return False class Cartel_Vote_Results(Page): @staticmethod def vars_for_template(player: Player): group_id = player.id_in_group return { 'group_id': group_id, } @staticmethod def js_vars(player: Player): return dict(mode=player.group.session.config['mode'], cartel_formed = player.group.cartel_formed) def is_displayed(player: Player): if player.round_number == 1: return True prev_group = player.group.prev_round_group did_not_form_cartel = not prev_group.cartel_formed cartel_was_discovered = prev_group.cartel_discovered and prev_group.cartel_formed if did_not_form_cartel or cartel_was_discovered: return True return False class WaitForChat(WaitPage): body_text = "Waiting for the other participants." class Calculator_Chat(Page): form_model = 'player' form_fields = ['calculator_me', 'calculator_r1', 'calculator_r2'] '''@staticmethod def get_timeout_seconds(player): if player.round_number == 1 or player.subsession.period == 1: player.calculator_chat_timeout_seconds = 60 else: player.calculator_chat_timeout_seconds = 30 return player.calculator_chat_timeout_seconds''' #### CHAT and CALCULATOR #### @staticmethod def js_vars(player: Player): return dict( my_id = player.id_in_group, mode = player.group.session.config['mode'], cartel_formed = player.group.cartel_formed, ) def live_method(player: Player, data): my_id = player.id_in_group group = player.group if 'text' in data: text = data['text'] msg = Message.create(group=group, sender=player, text=text) return {0: [to_dict(msg)]} return {my_id: [to_dict(msg) for msg in Message.filter(group=group)]} @staticmethod def vars_for_template(player: Player): nickname = f"Firm {player.id_in_subsession}" return dict(my_nickname=nickname) class Set_Price(Page): form_model = 'player' form_fields = ['price'] @staticmethod def js_vars(player: Player): return dict( my_id = player.id_in_group, mode = player.group.session.config['mode'], cartel_formed = player.group.cartel_formed, ) class ResultsWaitBertrand(WaitPage): body_text = "Waiting for the other participants to decide." def after_all_players_arrive(self): group = self.group group.unit_price() group.total_units() group.set_units() group.set_revenue() group.set_cost() group.set_no_fine_payoffs() group.check_if_cartel_discovered() if group.cartel_discovered: group.set_cartel_fines() group.set_payoffs() class Bertrand_Results(Page): @staticmethod def vars_for_template(player: Player): group_id = player.id_in_group other_group_ids = [p.id_in_group for p in player.group.get_players() if p.id_in_group != player.id_in_group] template = {} for (j, other_player) in enumerate(player.other_players): template[f"other_player_{j + 1}_price"] = other_player.price template[f"other_player_{j + 1}_no_fine_payoff"] = other_player.no_fine_payoff template[f"other_player_{j + 1}_payoff"] = other_player.payoff return { 'group_id': group_id, 'other_group_ids': other_group_ids, **template } class Antitrust(Page): @staticmethod def vars_for_template(player: Player): result = { 'cartel_formed': player.group.cartel_formed, 'cartel_discovered': player.group.cartel_discovered, 'cartel_fine': player.cartel_fine if player.group.cartel_discovered else 0., 'payoff_this_round': player.payoff, 'total_payoff': player.running_payoff } return result class History(Page): timeout_seconds = 60 @staticmethod def vars_for_template(player: Player): subsession = player.subsession current_sg = subsession.sg other_group_ids = [p.id_in_group for p in player.group.get_players() if p.id_in_group != player.id_in_group] history = [] for j, r in enumerate(player.in_all_rounds()): if r.subsession.sg == current_sg: other_prices = {f"other_player_{k + 1}_price": other_player.price for k, other_player in enumerate(r.get_others_in_group())} history.append(( r.subsession.period, r.price, r.formatted_units, r.payoff, r.running_payoff, r.cartel_fine, r.group.cartel_formed, r.group.cartel_discovered, other_prices, )) return { 'other_group_ids': other_group_ids, 'history': history, } class End_of_Supergame(Page): timeout_seconds = 60 @staticmethod def vars_for_template(player: Player): subsession = player.subsession current_sg = subsession.sg history = [] for j, r in enumerate(player.in_all_rounds()): if r.subsession.sg == current_sg: history.append(( r.subsession.period, r.price, r.formatted_units, r.payoff, r.running_payoff, r.cartel_fine, r.group.cartel_formed, r.group.cartel_discovered, )) return {'history': history} @staticmethod def is_displayed(player: Player): subsession = player.subsession return subsession.is_last_period == True page_sequence = [ Introduction, MatchingWaitPage, New_Supergame, Cartel_Vote, ResultsWaitVote, Cartel_Vote_Results, WaitForChat, Calculator_Chat, Set_Price, ResultsWaitBertrand, Bertrand_Results, Antitrust, History, End_of_Supergame, ]