from otree.api import * import math from decimal import Decimal, ROUND_HALF_UP doc = """ Baseline price-setting game: 8 independent producers, no teams, no explanations, no GPT. """ class C(BaseConstants): NAME_IN_URL = 'Uncertainty_baseline_BC_LI' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 10 class Subsession(BaseSubsession): pass class Group(BaseGroup): clear = models.BooleanField() average_price = models.FloatField() optimization = models.BooleanField() # True = OT, False = BC display_shock = models.BooleanField() class Player(BasePlayer): omega_current = models.IntegerField(default=40) omega_pre = models.IntegerField(default=40) omega_small = models.IntegerField(default=20) omega_large = models.IntegerField(default=60) omega_average = models.IntegerField(default=40) cost_deduction = models.IntegerField(default=10) price_aim_parameter_pre = models.IntegerField(default=15) # before shock price_aim_parameter_post = models.IntegerField(default=25) # after shock price_aim_parameter_current = models.IntegerField(default=15) price_aim_parameter_small = models.IntegerField(default=5) maximum_price = models.IntegerField() shock = models.IntegerField(default=6) optimization_instruction = models.BooleanField() # copy of group.optimization optimization = models.BooleanField() clear = models.BooleanField() clear_instruction = models.BooleanField() sitzplatznummer = models.IntegerField() # decision variables price = models.IntegerField(label="", min=10) output = models.IntegerField() unit_costs = models.IntegerField() total_costs = models.IntegerField() revenue = models.IntegerField() earnings = models.IntegerField(min=0) per_piece_earnings = models.IntegerField() price_aim = models.IntegerField() # payoff tracking payoff_round = models.IntegerField() payoff_acc = models.IntegerField() exchange_rate = models.IntegerField(default=200) show_up_fee = models.CurrencyField(default=5) payoff_mon = models.CurrencyField() payoff_mon_final = models.CurrencyField() payoff_mon_final_otree = models.CurrencyField() # quiz bonus attempts = models.IntegerField(blank=True) Q1_first_attempt = models.IntegerField(blank=True) Q2_first_attempt = models.IntegerField(blank=True) Q3_first_attempt = models.IntegerField(blank=True) Q4_first_attempt = models.IntegerField(blank=True) Q5_first_attempt = models.IntegerField(blank=True) Q6_first_attempt = models.IntegerField(blank=True) bonus = models.IntegerField(blank=True) bonus_mon = models.CurrencyField(blank=True) bonus_eligible = models.BooleanField(blank=True) payoff_bonus = models.IntegerField(blank=True) # BC quiz Q1_BC_first_attempt = models.IntegerField(blank=True) Q2_BC_first_attempt = models.IntegerField(blank=True) Q3_BC_first_attempt = models.IntegerField(blank=True) Q4_BC_first_attempt = models.IntegerField(blank=True) Q5_BC_first_attempt = models.IntegerField(blank=True) # Questionnaire Question_1 = models.IntegerField( choices=[[1, "Die Produktion erfolgt automatisch in Höhe der Nachfrage."], [2, "Die Produktion wird zufällig bestimmt."], [3, "Die Produktion hängt vom Durchschnittspreis ab."]], label="Frage 1: Wie wird die Höhe Ihrer Produktion bestimmt?", widget=widgets.RadioSelect ) Question_2 = models.IntegerField( choices=[[1, "Je höher der Preis p, desto höher die Nachfrage y."], [2, "Je höher der Preis p, desto niedriger die Nachfrage y."], [3, "Je niedriger der Preis p, desto niedriger die Nachfrage y."]], label="Frage 2: Welche Aussage über Ihre Nachfrage y und Ihren Preis p ist richtig?", widget=widgets.RadioSelect ) Question_3 = models.IntegerField( choices=[[1, "Je höher der exogene Faktor ω, desto höher die Nachfrage y."], [2, "Je höher der exogene Faktor ω, desto niedriger die Nachfrage y."], [3, "Je niedriger der exogene Faktor ω, desto höher die Nachfrage y."]], label="Frage 3: Welche Aussage über Ihre Nachfrage und den exogenen Faktor ω ist korrekt?", widget=widgets.RadioSelect ) Question_4 = models.IntegerField( choices=[[1, "Der Preis des eigenen Produkts."], [2, "Der Durchschnittspreis aller Produzenten über alle Runden hinweg."], [3, "Der Durchschnittspreis aller Produzenten in der jeweiligen Runde."]], label="Frage 4: Welche der folgenden Variablen hat einen Einfluss auf die Kosten pro Stück Ihres Produkts?", widget=widgets.RadioSelect ) Question_5 = models.IntegerField( choices=[[1, "... werden durch die Preissetzung aller Produzenten bestimmt und sind daher am Anfang jeder Runde unbekannt."], [2, "... sind zu Beginn jeder Runde bekannt."], [3, "... resultieren aus dem von Ihnen festgelegten Preis."]], label="Frage 5: Die Produktionskosten ...", widget=widgets.RadioSelect ) Question_6 = models.IntegerField( choices=[[1, "Trifft auf dieses Experiment nicht zu."], [2, "Trifft auf dieses Experiment nicht zu."], [3, "Trifft auf dieses Experiment nicht zu."]], label="Frage 6 (Platzhalter – hier kannst du für den Baseline eigene Inhalte definieren.)", widget=widgets.RadioSelect ) # BC questions Question_1_BC = models.IntegerField( choices=[[1, "Je näher Ihr Preis p am Zielpreis z liegt, desto höher Ihr Gewinn."], [2, "Je näher Ihr Preis p am Zielpreis z liegt, desto niedriger Ihr Gewinn."], [3, "Es gibt nur einen Gewinn, wenn Ihr Preis p exakt dem Zielpreis z entspricht."]], label="Frage 1: Welche Aussage über Ihren Gewinn ist korrekt?", widget=widgets.RadioSelect ) Question_2_BC = models.IntegerField( choices=[[1, "Je höher der Durchschnittspreis P, desto höher der Zielpreis z."], [2, "Je höher der Durchschnittspreis P, desto niedriger der Zielpreis z."], [3, "Der Zielpreis z ist unabhängig vom Durchschnittspreis P."]], label="Frage 2: Welche Aussage über das Verhältnis zwischen dem Zielpreis z und dem Durchschnittspreis P ist korrekt?", widget=widgets.RadioSelect ) Question_3_BC = models.IntegerField( choices=[ [1, "Der Zielpreis z liegt 15 Taler unter der Hälfte des Durchschnittspreises P."], [2, "Der Zielpreis z liegt 15 Taler über der Hälfte des Durchschnittspreises P."], [3, "Der Zielpreis z liegt 15 Taler über dem Durchschnittspreis P."]], label="Frage 3: Wie berechnet sich Ihr Zielpreis z?", widget=widgets.RadioSelect, allow_html=True, ) Question_4_BC = models.IntegerField( choices=[[1, "… wird durch die Preissetzung aller Produzenten bestimmt und ist daher zu Beginn jeder Runde unbekannt."], [2, "… ist zu Beginn jeder Runde bekannt."], [3, "… entspricht immer dem Zielpreis."]], label="Frage 4: Der Durchschnittspreis P ...", widget=widgets.RadioSelect ) Question_5_BC = models.IntegerField( choices=[[1, "Trifft auf dieses Experiment nicht zu."], [2, "Trifft auf dieses Experiment nicht zu."], [3, "Trifft auf dieses Experiment nicht zu."]], label="Frage 5 (Platzhalter – Baseline ohne Erklärungsbonus.)", widget=widgets.RadioSelect ) # Demographics Gender = models.IntegerField( choices=[[1, "Weiblich"], [2, "Männlich"], [3, "Divers"]], label="Welchem Geschlecht fühlen Sie sich zugehörig?", widget=widgets.RadioSelect ) Age = models.IntegerField(label="Wie alt sind Sie?", min=10, max=99) Faculty = models.IntegerField( choices=[[1, "Sozial- und Bildungswissenschaftliche Fakultät"], [2, "Geistes- und Kulturwissenschaftliche Fakultät"], [3, "Wirtschaftswissenschaftliche Fakultät"], [4, "Juristische Fakultät"], [5, "Fakultät für Informatik und Mathematik"], [6, "An mehreren Fakultäten"], [7, "Sonstiges"]], label="An welcher Fakultät studieren Sie?", widget=widgets.RadioSelect ) Makro = models.IntegerField( choices=[[1, "Ja"], [2, "Nein"]], label="Haben Sie bereits die Veranstaltung Makroökonomik besucht?", widget=widgets.RadioSelect ) tech_problems = models.LongStringField( label="Haben Sie sonstige Anmerkungen oder sind Ihnen technische Fehler aufgefallen?", max_length=2000, blank=True, ) # bounds for price input (if you want to use in templates) def price_max(player: Player): return player.omega_current def price_min(player: Player): return player.cost_deduction # assign treatments and shock def creating_session(subsession: Subsession): if subsession.round_number == 1: import itertools # randomize info treatment across groups clear_cycle = itertools.cycle([ False, False, True]) optimization_cycle = itertools.cycle([False, False, True, False]) # same as your original for group in subsession.get_groups(): group.clear = next(clear_cycle) group.optimization = next(optimization_cycle) else: # copy group treatments from round 1 for group in subsession.get_groups(): group.clear = group.in_round(1).clear group.optimization = group.in_round(1).optimization # set omega and price_aim_parameter_current depending on round (shock at 6) for group in subsession.get_groups(): for p in group.get_players(): if subsession.round_number >= 6: p.omega_current = p.omega_large p.price_aim_parameter_current = p.price_aim_parameter_post else: p.omega_current = p.omega_pre p.price_aim_parameter_current = p.price_aim_parameter_pre # display shock only in the shock round for group in subsession.get_groups(): group.display_shock = (subsession.round_number == 6) # copy group-level flags to player-level for group in subsession.get_groups(): for p in group.get_players(): p.optimization_instruction = group.optimization p.clear_instruction = group.clear def round_results(group: Group): players = group.get_players() # enforce minimum price for p in players: if p.price is not None and p.price < 10: p.price = 10 # compute average price if players: avg = sum(p.price for p in players) / len(players) group.average_price = int(Decimal(avg).to_integral_value(rounding=ROUND_HALF_UP)) else: group.average_price = 0 # compute outcomes for p in players: p.output = p.omega_current - p.price if p.output < 0: p.output = 0 p.revenue = p.output * p.price p.unit_costs = group.average_price - p.cost_deduction p.total_costs = p.unit_costs * p.output p.per_piece_earnings = p.price - p.unit_costs p.earnings = (p.price - p.unit_costs) * p.output if p.earnings < 0: p.earnings = 0 # BC: price_aim p.price_aim = round( (p.omega_current + group.average_price - p.cost_deduction) / 2 ) # accumulate payoffs for p in players: p.payoff_round = p.earnings if group.round_number > 1: all_rounds = p.in_all_rounds() p.payoff_acc = sum(r.payoff_round for r in all_rounds) else: p.payoff_acc = p.payoff_round if p.in_round(1).attempts and p.in_round(1).attempts > 1: p.bonus_eligible = False p.bonus = 0 else: p.bonus_eligible = True p.bonus = 400 p.bonus_mon = cu(p.bonus).to_real_world_currency(p.session) / p.exchange_rate p.payoff_mon = cu(p.payoff_acc).to_real_world_currency(p.session) / p.exchange_rate if p.payoff_mon < 0.1: p.payoff_mon = 3.5 if group.round_number == C.NUM_ROUNDS: p.payoff_mon_final = math.ceil(p.payoff_mon + p.bonus_mon + p.show_up_fee) p.payoff_mon_final_otree = math.ceil(p.payoff_mon + p.bonus_mon) p.payoff = p.payoff_mon_final_otree # PAGES class Place(Page): form_model = 'player' form_fields = ['sitzplatznummer'] @staticmethod def before_next_page(player: Player, timeout_happened): player.participant.label = str(player.sitzplatznummer) @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Start(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Pre_Comprehension(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Comprehension_OT(Page): @staticmethod def is_displayed(player: Player): return (player.round_number == 1) and player.group.optimization form_model = 'player' form_fields = [ 'Question_1', 'Question_2', 'Question_3', 'Question_4', 'Question_5', 'Question_6', 'attempts', 'Q1_first_attempt', 'Q2_first_attempt', 'Q3_first_attempt', 'Q4_first_attempt', 'Q5_first_attempt', 'Q6_first_attempt' ] class Comprehension_BC(Page): @staticmethod def is_displayed(player: Player): return (player.round_number == 1) and (not player.group.optimization) form_model = 'player' form_fields = [ 'Question_1_BC', 'Question_2_BC', 'Question_3_BC', 'Question_4_BC', 'Question_5_BC', 'attempts', 'Q1_BC_first_attempt', 'Q2_BC_first_attempt', 'Q3_BC_first_attempt', 'Q4_BC_first_attempt', 'Q5_BC_first_attempt' ] class StartWait(WaitPage): @staticmethod def is_displayed(player: Player): return player.round_number == 1 title_text = "Bitte warten Sie einen Moment." body_text = ( "Bitte warten Sie, bis die anderen Spieler bereit sind, um mit dem Experiment zu beginnen. " "Das Experiment wird in Kürze beginnen." ) class ShockAnnouncement(Page): @staticmethod def is_displayed(player: Player): return player.round_number == player.shock class PricePage(Page): form_model = 'player' form_fields = ['price'] class ResultsWait(WaitPage): after_all_players_arrive = 'round_results' title_text = "Bitte warten Sie einen Moment." body_text = ( "Bitte warten Sie kurz bis alle Teilnehmenden eine Entscheidung getroffen haben. " "Es geht in Kürze weiter." ) class ResultsFeedback(Page): pass class FinalResults(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS class Demographics(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS form_model = 'player' form_fields = ['Gender', 'Age', 'Faculty', 'Makro', 'tech_problems'] class FinalPage(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS page_sequence = [ Place, Start, Pre_Comprehension, Comprehension_OT, Comprehension_BC, StartWait, ShockAnnouncement, PricePage, ResultsWait, ResultsFeedback, FinalResults, Demographics, FinalPage, ]