import random from otree.api import * doc = """ The principal offers a contract to the agent, who can decide if to reject or accept. The agent then chooses an effort level. The implementation is based on Gaechter and Koenigstein (2006) . """ class C(BaseConstants): NAME_IN_URL = "principal_agent" PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 12 CONVERSION_RATE = 2.5 PAYING_ROUNDS = (2, 4, 9, 11, 13) BASE_PAY = cu(40) CHARITY_ENDOWMENT = cu(40) MIN_FIXED_PAYMENT = cu(0) MAX_FIXED_PAYMENT = cu(40) MIN_RETURN = cu(0) MAX_RETURN = cu(140) # """Amount principal gets if contract is rejected""" REJECT_PRINCIPAL_PAY = cu(0) REJECT_AGENT_PAY = cu(0) EFFORT_TO_RETURN = { 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60, 7: 70, 8: 80, 9: 90, 10: 100, } EFFORT_TO_COST = {1: 0, 2: 1, 3: 2, 4: 4, 5: 6, 6: 8, 7: 10, 8: 13, 9: 16, 10: 20} PRINCIPAL_ROLE = "principal" AGENT_ROLE = "agent" def cost_from_effort(effort): return cu(C.EFFORT_TO_COST[effort]) * (1 if effort > 0 else 0) def return_from_effort(effort): return cu(C.EFFORT_TO_RETURN[effort]) * (1 if effort > 0 else 0) class Subsession(BaseSubsession): pass class Group(BaseGroup): bonus_paid = models.CurrencyField() total_return = models.CurrencyField( doc="""Total return from agent's effort = [Return for single unit of agent's work effort] * [Agent's work effort]""" ) principal_return_target = models.CurrencyField( min=C.MIN_RETURN, max=C.MAX_RETURN, ) agent_fixed_pay = models.CurrencyField( doc="""Amount offered as fixed pay to agent""", min=C.MIN_FIXED_PAYMENT, # max=C.MAX_FIXED_PAYMENT, ) agent_bonus_fix = models.CurrencyField( doc="Amount offered as fixed pay to agent", min=C.MIN_FIXED_PAYMENT, initial=0, ) agent_bonus_var_exp = models.CurrencyField( doc="""Amount offered as fixed pay to agent""", min=C.MIN_FIXED_PAYMENT, initial=0, ) agent_bonus_var = models.CurrencyField( doc="""Amount offered as fixed pay to agent""", min=C.MIN_FIXED_PAYMENT, initial=0, ) agent_work_effort = models.IntegerField( initial=1, choices=range(1, 10 + 1), doc="""Agent's work effort, [1, 10]""", widget=widgets.RadioSelectHorizontal, ) agent_work_cost = models.CurrencyField( initial=0, doc="""Agent's cost of work effort""" ) agent_transfer = models.CurrencyField(initial=0, min=-100) principal_work_effort = models.IntegerField( choices=range(1, 10 + 1), doc="""Agent's work effort, [1, 10]""", widget=widgets.RadioSelectHorizontal, initial=1, ) principal_transfer = models.CurrencyField(initial=0, min=-100) contract_accepted = models.BooleanField( doc="""Whether agent accepts proposal""", widget=widgets.RadioSelect, choices=[ [True, "Kabul et"], [False, "Reddet"], ], ) def agent_fixed_pay_max(group): return group.principal_return_target def principal_transfer_max(group): return C.BASE_PAY def agent_transfer_max(group): return C.BASE_PAY class Player(BasePlayer): treatment = models.StringField() result = models.CurrencyField(initial=0) own_spillover = models.CurrencyField(initial=0) incentive_q_1 = models.IntegerField( choices=[ [0, "Toplam ödemeyi yalnızca Oyuncu B teklifi kabul ederse ödersiniz."], [0, "Bonus ödemesi Oyuncu B'nin kararlarına bağlıdır."], [1, "Toplam ödemenin miktarı Oyuncu B'nin kararlarına bağlıdır."], [ 0, "Belirli koşullar altında, B Oyuncusuna teklif edilen tazminatın B Oyuncusu tarafından elde edilen kârı aşması mümkündür.", ], ], verbose_name="Oyuncu A olduğunuzu ve bir tazminat teklifi yapmanız istendiğini varsayalım bu durumda aşağıdaki ifadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) incentive_q_2 = models.IntegerField( choices=[ [ 0, "Teklifi kabul ederseniz, en azından toplam ödemeyi ve ilk bağışınızı alırsınız.", ], [0, "Hiçbir şey yapmazsanız, teklifi otomatik olarak reddetmiş olursunuz."], [ 0, "Teklifi reddederseniz, yalnızca başlangıç miktarını alırsınız.", ], [ 1, "Kararlarınız ne olursa olsun, her durumda sözleşmeye bağlı bonus ödemesini alacaksınız.", ], ], verbose_name="Oyuncu B olduğunuzu ve Oyuncu A'nın tazminat teklifini kabul etmek isteyip istemediğinizin sorulduğunu varsayalım bu durumda aşağıdaki ifadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) incentive_q_3 = models.IntegerField( choices=[ [ 0, "Ne kadar çaba harcayacağınızı seçmekte özgürsünüz.", ], [0, "Yatırılan her birim turun kârını 10 taler artırır."], [ 0, "Ne kadar çok birim yatırırsanız, ek bir birim o kadar pahalı olur.", ], [ 1, "Kâr hedefine ulaşmak için en az gerektiği kadar birim yatırmak zorundasınız.", ], ], verbose_name="Oyuncu B olduğunuzu ve ne kadar çaba harcamak istediğinizin sorulduğunu varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) incentive_q_4 = models.IntegerField( choices=[ [ 0, "Kâr, tazminat sözleşmesinin üzerinde anlaşılan şartlarına göre Oyuncu A ve Oyuncu B arasında dağıtılır.", ], [0, "Oyuncu B, Oyuncu C'nin kazancını hem artırabilir hem de azaltabilir."], [ 0, "Pozitif bir transfer tutarı kârı artırır ve C oyuncusunun kazancını azaltır.", ], [ 1, "Oyuncu C'ye yapılan ödeme kurgusaldır ve doğrulanamaz.", ], ], verbose_name="Aşağıdaki ifadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) bonus_q_1 = models.IntegerField( choices=[ [ 0, "Toplam ödemeyi yalnızca Oyuncu B teklifi kabul ederse yaparsınız.", ], [ 0, "Gönüllü ikramiyenin miktarına turun sonuçları belli olduktan sonra karar verebilirsiniz.", ], [ 0, "Belirli koşullar altında, B Oyuncusuna teklif edilen tazminatın B Oyuncusu tarafından elde edilen kârı aşması mümkündür.", ], [ 1, "Gönüllü bonus ödemesi, kâr hedefine ulaşılırsa her takdirde ödenecektir.", ], ], verbose_name="Oyuncu A olduğunuzu ve bir tazminat teklifi yapmanız istendiğini varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) bonus_q_2 = models.IntegerField( choices=[ [ 0, "Teklifi kabul ederseniz, en azından toplam ödemeyi ve başlangıç miktarını alırsınız.", ], [0, "Hiçbir şey yapmazsanız, teklifi otomatik olarak reddedersiniz."], [ 0, "Teklifi reddederseniz, yalnızca ilk bağışınızı alırsınız.", ], [ 1, "Kararlarınız ne olursa olsun, her durumda gönüllü bonus ödemesini alırsınız.", ], ], verbose_name="B oyuncusu olduğunuzu ve A oyuncusunun tazminat teklifini kabul etmek isteyip istemediğinizin sorulduğunu varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) bonus_q_3 = models.IntegerField( choices=[ [ 0, "Ne kadar çaba harcayacağınızı seçmekte özgürsünüz.", ], [0, "Yatırılan her birim turun kârını 10 taler artırır."], [ 0, "Ne kadar çok birim yatırırsanız, ek bir birim o kadar pahalı olur.", ], [ 1, "Kâr hedefine ulaşmak için en az gerektiği kadar birim yatırmak zorundasınız.", ], ], verbose_name="Oyuncu B olduğunuzu ve ne kadar çaba harcamak istediğinizin sorulduğunu varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) bonus_q_4 = models.IntegerField( choices=[ [ 0, "Kâr, tazminat sözleşmesinin üzerinde anlaşılan şartlarına göre Oyuncu A ve Oyuncu B arasında dağıtılır.", ], [ 0, "Oyuncu B, Oyuncu C'nin kazancını hem artırabilir hem de azaltabilir.", ], [ 0, "Pozitif bir transfer tutarı kârı artırır ve C oyuncusunun kazancını azaltır.", ], [ 1, "Oyuncu C'ye yapılan ödeme kurgusaldır ve doğrulanamaz.", ], ], verbose_name="Aşağıdaki ifadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) flat_wage_q_1 = models.IntegerField( choices=[ [ 0, "Toplam ödemeyi yalnızca Oyuncu B teklifi kabul ederse ödersiniz.", ], [0, "Toplam ödeme en fazla kâr hedefine eşit olabilir."], [ 1, "Toplam ödemenin miktarı, Oyuncu B'nin çabasına ve transfer kararlarına bağlıdır.", ], [ 0, "Belirli koşullar altında, ödemenin kârı aşması mümkündür.", ], ], verbose_name="Oyuncu A olduğunuzu ve bir tazminat teklifi yapmanız istendiğini varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) flat_wage_q_2 = models.IntegerField( choices=[ [ 0, "Teklifi kabul ederseniz, en azından toplam ödemeyi ve başlangıç miktarını alırsınız.", ], [0, "Hiçbir şey yapmazsanız, teklifi otomatik olarak reddedersiniz."], [ 0, "Teklifi reddederseniz, yalnızca başlangıç miktarını alırsınız.", ], [ 1, "Sözleşme teklifini kabul ederseniz kâr hedefini uygulamakla yükümlü olursunuz.", ], ], verbose_name="Oyuncu B olduğunuzu ve Oyuncu A'nın tazminat teklifini kabul edip etmeyeceğinizin sorulduğunu varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) flat_wage_q_3 = models.IntegerField( choices=[ [ 0, "Ne kadar çaba harcayacağınızı seçmekte özgürsünüz.", ], [0, "Yatırılan her birim, turun kârını 10 taler artırır."], [ 0, "Ne kadar çok çaba sarf ederseniz, ek bir birim çaba sarf etmek o kadar maliyetli olur.", ], [ 1, "Kâr hedefine ulaşmak için gereken eforu sarf etmek zorundasınız.", ], ], verbose_name="B Oyuncusu olduğunuzu ve ne kadar çaba harcamak istediğinizin sorulduğunu varsayalım. İfadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) flat_wage_q_4 = models.IntegerField( choices=[ [ 0, "Kâr, tazminat sözleşmesinin üzerinde anlaşılan şartlarına göre Oyuncu A ve Oyuncu B arasında dağıtılır.", ], [ 0, "Oyuncu B, Oyuncu C'nin ödemesini hem artırabilir hem de azaltabilir.", ], [ 0, "Pozitif bir transfer tutarı kârı artırır ve C oyuncusunun kazancını azaltır.", ], [ 1, "Oyuncu C'ye yapılan ödeme kurgusaldır ve doğrulanamaz.", ], ], verbose_name="Aşağıdaki ifadelerden hangisi yanlıştır?", widget=widgets.RadioSelect, ) # FUNCTIONS def creating_session(subsession: Subsession): subsession.group_randomly(fixed_id_in_group=True) if subsession.round_number == 1: for player in subsession.get_players(): participant = player.participant # bonus, incentive, flat_wage participant.vars["treatment"] = subsession.session.config["treatment"] player.treatment = subsession.session.config["treatment"] def set_bonus(group: Group): principal = group.get_player_by_role(C.PRINCIPAL_ROLE) agent = group.get_player_by_role(C.AGENT_ROLE) principal.result += group.agent_bonus_var * (-1) agent.result += group.agent_bonus_var def set_payoffs(group: Group): principal = group.get_player_by_role(C.PRINCIPAL_ROLE) agent = group.get_player_by_role(C.AGENT_ROLE) total_return = group.field_maybe_none("total_return") or 0 group.bonus_paid = ( bool(total_return >= group.principal_return_target) * group.agent_bonus_fix ) # def bonus_paid(self): # return bool(self.total_return >= self.principal_return_target)*self.agent_bonus_fix if not group.contract_accepted: principal.result = C.REJECT_PRINCIPAL_PAY agent.result = C.REJECT_AGENT_PAY else: group.agent_work_cost = cost_from_effort(group.agent_work_effort) group.total_return = ( return_from_effort(group.agent_work_effort) + group.agent_transfer ) within_target = bool(group.total_return >= group.principal_return_target) money_to_agent = ( group.agent_fixed_pay + group.agent_bonus_fix * within_target + group.agent_bonus_var ) agent.result = money_to_agent - group.agent_work_cost if agent.result < 0: # edited: prevent payoff to be negative agent.result = 0 principal.result = group.total_return - money_to_agent principal.result += C.BASE_PAY agent.result += C.BASE_PAY def role(player: Player): if player.id_in_group == 1: return "principal" if player.id_in_group == 2: return "agent" # PAGES class Introduction(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 def vars_for_template(player: Player): return { "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } class Questionnaire(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 timeout_seconds = 5 * 60 def vars_for_template(player: Player): return { "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } form_model = "player" def get_form_fields(player): treatment = player.participant.vars["treatment"] form_fields = ["q_1", "q_2", "q_3", "q_4"] treatment_form_fields = [f"{treatment}_{f}" for f in form_fields] return treatment_form_fields @staticmethod def error_message(player: Player, values): treatment = player.participant.vars["treatment"] if ( values[f"{treatment}_q_1"] + values[f"{treatment}_q_2"] + values[f"{treatment}_q_3"] + values[f"{treatment}_q_4"] != 4 ): return "Bir veya birden fazla cevabınız yanlış." class Offer(Page): @staticmethod def is_displayed(player: Player): return role(player) == "principal" form_model = "group" @staticmethod def vars_for_template(player: Player): return { "treatment": player.participant.vars["treatment"], "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } def get_form_fields(player): treatment = player.participant.vars["treatment"] # {{ group.principal_return_target }} if treatment == "incentive": return ["principal_return_target", "agent_fixed_pay", "agent_bonus_fix"] if treatment == "bonus": return ["principal_return_target", "agent_fixed_pay", "agent_bonus_var_exp"] if treatment == "flat_wage": return ["principal_return_target", "agent_fixed_pay"] @staticmethod def error_message(player: Player, values): treatment = player.participant.vars["treatment"] if values["principal_return_target"] < values["agent_fixed_pay"]: return "Toplam ödeme kâr hedefini aşmamalıdır." if treatment == "incentive": if values["principal_return_target"] < ( values["agent_fixed_pay"] + values["agent_bonus_fix"] ): return "Toplam ödeme ve ön ödemenin toplamı kâr hedefini aşmamalıdır." if treatment == "bonus": if values["principal_return_target"] < ( values["agent_fixed_pay"] + values["agent_bonus_var_exp"] ): return "Toplam ödeme ve primin toplamın kâr hedefini aşmamalıdır." timeout_seconds = 60 def before_next_page(player, timeout_happened): treatment = player.participant.vars["treatment"] group = player.group if timeout_happened: if treatment == "incentive": group.principal_return_target = 0 group.agent_fixed_pay = 0 group.agent_bonus_fix = 0 if treatment == "bonus": group.principal_return_target = 0 group.agent_fixed_pay = 0 group.agent_bonus_var_exp = 0 if treatment == "flat_wage": group.principal_return_target = 0 group.agent_fixed_pay = 0 class OfferWaitPage(WaitPage): @staticmethod def vars_for_template(player: Player): if role(player) == "agent": body_text = "Siz Oyuncu B'siniz. Oyuncu A size kısa süre içerisinde teklif sunacaktır." else: body_text = "Oyuncu B kısa süre içerisinde yanıt verecektir." return {"body_text": body_text} class Accept(Page): @staticmethod def is_displayed(player: Player): return role(player) == "agent" form_model = "group" form_fields = ["contract_accepted"] timeout_seconds = 30 timeout_submission = { "contract_accepted": False, } def vars_for_template(player: Player): return { "treatment": player.participant.vars["treatment"], "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } class AgentEffort(Page): @staticmethod def is_displayed(player: Player): return role(player) == "agent" and player.group.contract_accepted == True form_model = "group" form_fields = ["agent_work_effort", "agent_transfer"] timeout_seconds = 60 timeout_submission = { "agent_work_effort": 1, "agent_transfer": 0, } @staticmethod def error_message(player: Player, values): if 10 * values["agent_work_effort"] < (-1) * values["agent_transfer"]: return "Your transfer to Player C will exceed the profit you make with the chosen effort." @staticmethod def vars_for_template(player: Player): return { "treatment": player.participant.vars["treatment"], "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } class PrincipalEffort(Page): @staticmethod def is_displayed(player: Player): return role(player) == "principal" and player.group.contract_accepted == True form_model = "group" form_fields = ["principal_work_effort", "principal_transfer"] @staticmethod def error_message(player: Player, values): if 10 * values["principal_work_effort"] < (-1) * values["principal_transfer"]: return "Your transfer to Player C will exceed the profit you make with the chosen effort." timeout_seconds = 60 timeout_submission = { "principal_work_effort": 1, "principal_transfer": 0, } def vars_for_template(player: Player): return { "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): set_payoffs(group) class Results(Page): timeout_seconds = 15 @staticmethod def vars_for_template(player: Player): is_paid = bool(player.round_number in C.PAYING_ROUNDS) player.payoff = is_paid * player.result / C.CONVERSION_RATE player.own_spillover = is_paid * (40 - player.group.agent_transfer) / 2 return { "received": player.result - C.BASE_PAY, "effort_cost": cost_from_effort(player.group.agent_work_effort), "treatment": player.participant.vars["treatment"], "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } class FinalResults(Page): @staticmethod def is_displayed(player: Player): return ( player.round_number == player.subsession.session.config["number_of_rounds"] ) @staticmethod def vars_for_template(player: Player): total_spillover = 2 * sum(p.own_spillover for p in player.in_all_rounds()) player.participant.vars["total_spillover"] = round( float(total_spillover / C.CONVERSION_RATE)/2, 2 ) player.participant.vars["main_result"] = float( sum(p.payoff for p in player.in_all_rounds()) ) return { "total_spillover": player.participant.vars["total_spillover"], "payoff": float(sum(p.payoff for p in player.in_all_rounds())), } @staticmethod def app_after_this_page(player, upcoming_apps): if len(upcoming_apps) > 0: return upcoming_apps[0] else: return None class ResultsPrincipal(Page): @staticmethod def is_displayed(player: Player): return ( role(player) == "principal" and player.group.contract_accepted == True and player.participant.vars["treatment"] == "bonus" ) timeout_seconds = 30 form_model = "group" @staticmethod def get_form_fields(player: Player): if player.group.contract_accepted == True: return ["agent_bonus_var"] @staticmethod def vars_for_template(player: Player): return { "received": player.payoff - C.BASE_PAY, "number_of_rounds": player.subsession.session.config["number_of_rounds"], "instruction_template": f"principal_agent/instructions/{player.participant.vars['treatment']}.html", } @staticmethod def before_next_page(player: Player, timeout_happened): set_bonus(player.group) class BonusWaitPage(WaitPage): def is_displayed(player: Player): return player.participant.vars["treatment"] == "bonus" @staticmethod def vars_for_template(player: Player): if role(player) == "agent": body_text = ( "Oyuncu A şu an bonus ödemesine karar vermektedir." ) else: body_text = "Oyuncu B size birazdan cevap verecektir." return {"body_text": body_text} page_sequence = [ Introduction, Questionnaire, Offer, OfferWaitPage, Accept, OfferWaitPage, PrincipalEffort, AgentEffort, ResultsWaitPage, ResultsPrincipal, BonusWaitPage, Results, FinalResults, ]