from otree.api import * import random doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'part4' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 NUM_SLIDERS = 48 # Anzahl der Slider POINTS_PER_HIT = 15 # <- Vergütung pro Slider class Subsession(BaseSubsession): pass def creating_session(subsession): for player in subsession.get_players(): player.participant.vars['participation_draw'] = random.random() def vars_for_admin_report(subsession): session = subsession.session players = subsession.get_players() rate = session.config['real_world_currency_per_point'] fee = session.config['participation_fee'] rows = [] for p in players: v = p.participant.vars # Live Auszahlung (wächst während des Experiments an) payoff = float(p.participant.payoff) * rate + fee # Bereits evaluierte Spenden aus Teil 1 und Teil 3 p1_taking = v.get('Taking_1') p1_eur = None if p1_taking is not None: p1_det = v.get('Detect_1') d1 = 400 if (p1_taking == 1 and p1_det == 1) else (0 if p1_taking == 1 else 500) p1_eur = d1 * rate p3_taking = v.get('part3_taking_2') p3_eur = None if p3_taking is not None: p3_det = v.get('part3_detect_2') d3 = 430 if (p3_taking and p3_det) else (0 if p3_taking else 570) p3_eur = d3 * rate donation = None if p1_eur is not None and p3_eur is not None: donation = p1_eur + p3_eur grand_total = "-" if donation is not None: grand_total = f"{(payoff + donation):.2f} €" app = p.participant._current_app_name page = p.participant._current_page_name status_badge = "warning" status_text = f"{app or ''} / {page or ''}" is_finished = False rows.append({ 'code': p.participant.code, 'status_badge': status_badge, 'status_text': status_text, 'payoff': f"{payoff:.2f} €", 'total_donation': f"{donation:.2f} €" if donation is not None else "-", 'grand_total': grand_total, 'org1': v.get('donation_org_1') or "-", 'p1_eur': f"{p1_eur:.2f} €" if p1_eur is not None else "-", 'org3': v.get('donation_org_3') or "-", 'p3_eur': f"{p3_eur:.2f} €" if p3_eur is not None else "-", 'is_finished': is_finished }) return dict(rows=rows) class Group(BaseGroup): pass class Player(BasePlayer): Participate = models.IntegerField() num_slider_hits = models.IntegerField(initial=0) # Slider-Felder dynamisch erzeugen for i in range(1, C.NUM_SLIDERS + 1): locals()[f'slider_{i}'] = models.IntegerField( min=0, max=100, blank=True ) del i def count_hits(player): """Zähle, wie viele Slider exakt auf dem Zielwert liegen.""" targets = player.targets_for_player() hits = 0 for i, t in enumerate(targets, start=1): v = getattr(player, f"slider_{i}") if v is not None and v == t: # exakte Übereinstimmung hits += 1 return hits def determine_participation(player): draw = player.participant.vars.get('participation_draw') rule = player.participant.vars.get('decision_rule') punished = player.participant.vars.get('punished_1', False) if draw is None or rule is None: # Fallback if something is missing player.Participate = 0 elif rule == 1: # Rule 1: 90% for normal, 10% for punished. if not punished: player.Participate = int(draw < 0.9) else: player.Participate = int(draw < 0.1) elif rule == 2: # Rule 2: 50% for everyone. player.Participate = int(draw < 0.5) else: player.Participate = 0 # Store for reuse player.participant.vars['Participate'] = player.Participate print( f"Player {player.id_in_group} participation set to {player.Participate} (draw={draw}, rule={rule}, punished={punished})") def targets_for_player(player): rnd = random.Random(f"{player.participant.code}-sliders") # stabiler Seed return [rnd.randint(10, 90) for _ in range(C.NUM_SLIDERS)] # --- FAIRNESS (zwei Situationsentscheidungen) --------------------------- fairness_sit1 = models.StringField( choices=[('A', 'Variante A'), ('B', 'Variante B'), ('C', 'Variante C'), ('D', 'Variante D')], widget=widgets.RadioSelect, label='Situation 1: Welche Variante bevorzugen Sie?' ) fairness_sit2 = models.StringField( choices=[('A', 'Variante A'), ('B', 'Variante B'), ('C', 'Variante C'), ('D', 'Variante D')], widget=widgets.RadioSelect, label='Situation 2: Welche Variante bevorzugen Sie?' ) # --- SOCIAL NORM -------------------------------------------------------- social_norm = models.IntegerField( label='Wie sozial angemessen finden Sie es, wenn Personen, die gegen ein Strafgesetz verstoßen haben, ' 'von Abstimmungen ausgeschlossen werden, obwohl in diesen Abstimmungen Entscheidungen getroffen werden, ' 'die für alle Personen relevant sind?', widget=widgets.RadioSelectHorizontal, choices=[ [1, 'Sehr sozial unangemessen'], [2, 'Sozial unangemessen'], [3, 'Sozial angemessen'], [4, 'Sehr sozial angemessen'], ] ) # --- JUSTICE SENSITIVITY (8 Items, 0–5) --------------------------------- js_victim_1 = models.IntegerField(label='Es ärgert mich, wenn es anderen unverdient besser geht als mir.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_victim_2 = models.IntegerField( label='Es macht mir zu schaffen, wenn ich mich für Dinge abrackern muss, die anderen in den Schoß fallen.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_observer_1 = models.IntegerField( label='Ich bin empört, wenn es jemandem unverdient schlechter geht als anderen.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_observer_2 = models.IntegerField( label='Es macht mir zu schaffen, wenn sich jemand für Dinge abrackern muss, die anderen in den Schoß fallen.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_benef_1 = models.IntegerField(label='Ich habe Schuldgefühle, wenn es mir unverdient besser geht als anderen.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_benef_2 = models.IntegerField( label='Es macht mir zu schaffen, wenn mir Dinge in den Schoß fallen, für die andere sich abrackern müssen.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_perp_1 = models.IntegerField(label='Ich habe Schuldgefühle, wenn ich mich auf Kosten anderer bereichere.', choices=range(6), widget=widgets.RadioSelectHorizontal) js_perp_2 = models.IntegerField( label='Es macht mir zu schaffen, wenn ich mir durch Tricks Dinge verschaffe, für die sich andere abrackern müssen.', choices=range(6), widget=widgets.RadioSelectHorizontal) # --- SELBSTEINSCHÄTZUNGEN 0–10 ----------------------------------------- patience = models.IntegerField( label='Sind Sie im Allgemeinen ein Mensch, der ungeduldig ist, oder der immer sehr viel ' 'Geduld aufbringt?', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], widget=widgets.RadioSelectHorizontal) risk = models.IntegerField(label='Sind Sie im Allgemeinen ein ' 'risikobereiter Mensch oder versuchen Sie, Risiken zu vermeiden?', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], widget=widgets.RadioSelectHorizontal) impulsive = models.IntegerField( label='Sind Sie im Allgemeinen ein Mensch, der lange überlegt ' 'und nachdenkt, bevor er handelt, also gar nicht impulsiv ist? Oder sind Sie ein Mensch, der ohne lange ' 'zu überlegen handelt, also sehr impulsiv ist?', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], widget=widgets.RadioSelectHorizontal) # --- DEMOGRAFIE --------------------------------------------------------- age = models.IntegerField(label='Wie alt sind Sie?', min=16, max=99) gender = models.StringField( label='Sind Sie?', choices=['männlich', 'weiblich', 'divers'], widget=widgets.RadioSelectHorizontal ) study_semesters = models.IntegerField( label='Im wievielten Semester studieren Sie (Bachelor + Master)? Falls Sie nicht studieren, geben Sie 0 ein.', min=0, max=100) study_field = models.StringField( blank=True, label='Welches Fach studieren Sie?', choices=[ 'Asien-Afrika-Studien / Regionalstudien (z.B. Japanologie, Turkologie)', 'Betriebswirtschaftslehre (BWL)', 'Bewegungs- und Sportwissenschaft', 'Biologie und Molecular Life Sciences', 'Chemie, Pharmazie und Lebensmittelchemie', 'Erdsystem- und Geowissenschaften (Meteorologie, Ozeanographie, Geographie)', 'Erziehungs- und Bildungswissenschaft (Nicht-Lehramt)', 'Geschichte und Archäologie', 'Informatik und Software-System-Entwicklung', 'Kultur- und Kunstwissenschaften (z.B. Ethnologie, Musik, Kunstgeschichte)', 'Lehramt (alle Schulformen)', 'Mathematik und Wirtschaftsmathematik', 'Medien- und Kommunikationswissenschaften / Journalistik', 'Medizin, Zahnmedizin und Hebammenwissenschaft', 'Philosophie und Religionswissenschaft', 'Physik und Nanowissenschaften', 'Politikwissenschaft, Soziologie und Kriminologie', 'Psychologie', 'Rechtswissenschaft (Jura)', 'Sprach- und Literaturwissenschaften (z.B. Germanistik, Anglistik, Romanistik)', 'Volkswirtschaftslehre (VWL) und Sozialökonomie', 'Wirtschafts-Ingenieurwesen / Wirtschaftsinformatik', 'Sonstiges' ] ) study_field_other = models.StringField( blank=True, label='Sonstiges: Name Ihres Studienfachs?' ) final_payoff_euro = models.FloatField() total_donation_eur = models.FloatField(initial=0.0) donation_org_1 = models.StringField(initial="") donation_org_3 = models.StringField(initial="") donation_p1_eur = models.FloatField(initial=0.0) donation_p3_eur = models.FloatField(initial=0.0) # PAGES class Part4Intro(Page): def vars_for_template(player): player.determine_participation() class SliderTask(Page): form_model = 'player' form_fields = [f'slider_{i}' for i in range(1, C.NUM_SLIDERS + 1)] timeout_seconds = 120 def is_displayed(player): return player.Participate == 1 def vars_for_template(player): targets = player.targets_for_player() import random rnd = random.Random(f"{player.participant.code}-sliders-ui") starts = [rnd.randint(0, 100) for _ in range(C.NUM_SLIDERS)] shifts = [rnd.choice([-18, -12, -6, 0, 6, 12, 18]) for _ in range(C.NUM_SLIDERS)] sliders = [ {'n': i, 'target': t, 'start': starts[i - 1], 'shift': shifts[i - 1]} for i, t in enumerate(targets, start=1) ] ticks = [{'cls': ('major' if i % 2 == 0 else 'minor')} for i in range(11)] return dict( sliders=sliders, ticks=ticks, num_sliders=C.NUM_SLIDERS, ) def before_next_page(player, timeout_happened): hits = player.count_hits() points = hits * C.POINTS_PER_HIT player.payoff = points player.num_slider_hits = hits player.participant.vars['payoff_part_4'] = points player.participant.vars['part4_hits'] = hits # optional für Auswertung class Questionnaire(Page): form_model = 'player' form_fields = ['fairness_sit1', 'fairness_sit2', 'social_norm', 'js_victim_1', 'js_victim_2', 'js_observer_1', 'js_observer_2', 'js_benef_1', 'js_benef_2', 'js_perp_1', 'js_perp_2', 'patience', 'risk', 'impulsive', 'age', 'gender', 'study_semesters', 'study_field', 'study_field_other'] def error_message(player: Player, values): if values['study_semesters'] > 0: if not values['study_field']: return 'Bitte geben Sie Ihr Studienfach an.' if values['study_field'] == 'Sonstiges' and not values['study_field_other']: return 'Bitte geben Sie Ihren genauen Studiengang an.' def before_next_page(player: Player, timeout_happened): points = float(player.participant.payoff) rate = player.session.config['real_world_currency_per_point'] fee = player.session.config['participation_fee'] euro_value = (points * rate) + fee player.final_payoff_euro = float(euro_value) # Spendenberechnung und Organisation speichern v = player.participant.vars p1_taking = v.get('Taking_1') p1_detect = v.get('Detect_1') donation_p1 = 500 if p1_taking == 1: if p1_detect == 1: donation_p1 = 400 else: donation_p1 = 0 taking = v.get('part3_taking_2') detect = v.get('part3_detect_2') donation_p3 = 570 if taking: if detect: donation_p3 = 430 else: donation_p3 = 0 total_donation = donation_p1 + donation_p3 player.total_donation_eur = round(total_donation * rate, 2) player.donation_p1_eur = round(donation_p1 * rate, 2) player.donation_p3_eur = round(donation_p3 * rate, 2) player.donation_org_1 = v.get('donation_org_1', '') player.donation_org_3 = v.get('donation_org_3', '') class Payout(Page): def vars_for_template(player): v = player.participant.vars r = player.session.config['real_world_currency_per_point'] part3_payoff = v.get('payoff_part_3', 0) taking = v.get('part3_taking_2') detect = v.get('part3_detect_2') sel = v.get('part3_selected') # Punkte für Unterkategorien action_points = 0 belief_points = 0 if taking: if detect: action_points = -340 else: action_points = 430 if sel == 'Belief_Vote': b = v.get('part3_belief_vote') dec = v.get('decision_rule') if b is not None and dec is not None and b == dec: belief_points = 100 elif sel == 'Belief_Taking': b = v.get('part3_belief_taking') actual = v.get('part3_actual_category') if b is not None and actual is not None and b == actual: belief_points = 100 belief_taking_labels = { 1: "0–1", 2: "2–4", 3: "5–7", 4: "8–10", } b_label = belief_taking_labels.get(b, "—") actual_label = belief_taking_labels.get(actual, "—") # Texte wie zuvor (gekürzt) action_text = "Spendengeld genommen (entdeckt)" if taking and detect else \ "Spendengeld genommen (nicht entdeckt)" if taking else \ "Spendengeld nicht genommen" belief_text = "Glauben: —" if sel == 'Belief_Vote': b = v.get('part3_belief_vote') dec = v.get('decision_rule') belief_text = f"Abstimmungsergebnis — Ihre Vermutung: {b if b else '—'}; Tatsächlich: {dec if dec else '—'}" elif sel == 'Belief_Taking': b = v.get('part3_belief_taking') actual = v.get('part3_actual_category') belief_text = ( f"Wie viele „nehmen“? " f"Ihre Schätzung: {b_label}; " f"Tatsächlich: {actual_label}" ) # part 1 breakup p1_taking = v.get('Taking_1') p1_detect = v.get('Detect_1') p1_info = v.get('Info') treatment = player.session.config.get('treatment') # Baseline part1_items = [("Ausstattung", 1000)] # Information if treatment == 'INFO' and p1_info == 1: part1_items.append(("Informationen zu Teil 2 gekauft", -200)) # Taking donation_p1 = 500 if p1_taking == 1: if p1_detect == 1: part1_items.append(("Spendengeld genommen (entdeckt)", -300)) donation_p1 = 400 else: part1_items.append(("Spendengeld genommen (nicht entdeckt)", 400)) donation_p1 = 0 else: part1_items.append(("Spendengeld nicht genommen", 0)) # Part 3 Donation donation_p3 = 570 if taking: if detect: donation_p3 = 430 else: donation_p3 = 0 total_donation = donation_p1 + donation_p3 payoff_part1 = v.get('payoff_part_1', 0) payoff_part4 = v.get('payoff_part_4', 0) participation_points = int(player.session.config['participation_fee'] / r) total_points = payoff_part1 + part3_payoff + payoff_part4 + participation_points return { 'part1_items': part1_items, 'payoff_part1': payoff_part1, 'payoff_part1_eur': round(payoff_part1 * r, 2), 'payoff_part3': part3_payoff, 'payoff_part3_eur': round(part3_payoff * r, 2), 'payoff_part4': payoff_part4, 'payoff_part4_eur': round(payoff_part4 * r, 2), 'part4_hits': v.get('part4_hits', 0), 'participation_fee': player.session.config['participation_fee'], 'participation_points': participation_points, 'total_payoff_points': total_points, 'total_payoff': player.participant.payoff_plus_participation_fee(), 'part3_action_text': action_text, 'part3_belief_text': belief_text, 'part3_action_points': action_points, 'part3_belief_points': belief_points, 'donation_p1': donation_p1, 'donation_p1_eur': player.donation_p1_eur, 'donation_org_1': player.donation_org_1, 'donation_p3': donation_p3, 'donation_p3_eur': player.donation_p3_eur, 'donation_org_3': player.donation_org_3, 'total_donation': total_donation, 'total_donation_eur': player.total_donation_eur, 'grand_total_eur': round(player.final_payoff_euro + player.total_donation_eur, 2), 'grand_total_points': total_points + total_donation, } class Outro(Page): pass page_sequence = [Part4Intro, SliderTask, Questionnaire, Payout, Outro]