from otree.api import * import random doc = """ Two-table post-game survey using the user's original strategy: - Survey (General): AI-neutral behaviour + perception (matrix table, Agree↔Disagree) - SurveyAI: AI-specific behaviour only (matrix table, Agree↔Disagree), only if participant.vars['has_ai'] - AISymmetric: AI vs Human comparative (matrix table, AI↔Human), only if n_ai_opponents == 1 """ class C(BaseConstants): NAME_IN_URL = 'PostSurvey' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 def creating_session(subsession): # You set participant.vars['treatment'] / ['has_ai'] / ['n_ai_opponents'] upstream. # We avoid KeyErrors here by not assuming presence. for p in subsession.get_players(): p.treatment = p.participant.vars.get('treatment', '') # No default set for has_ai / n_ai_opponents here; read them where needed. # fix orientation once per participant if 'ai_first' not in p.participant.vars: p.participant.vars['ai_first'] = random.choice([True, False]) # ---------- helpers ---------- def make_likert(label): # Keep your horizontal 6-point scale & labels (matching your table header). return models.IntegerField( choices=[ [1, "Completely Disagree"], [2, "Largely Disagree"], [3, "Slightly Disagree"], [4, "Slightly Agree"], [5, "Largely Agree"], [6, "Completely Agree"], ], label=label, widget=widgets.RadioSelectHorizontal ) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): treatment = models.StringField() # ===================== General (AI-neutral) — Behaviour (self-report) ===================== beh_first_proposal = make_likert('I often tried to be the first to make a proposal.') beh_counter_proposal = make_likert('I sometimes countered others’ proposals to change the outcome.') beh_strategy_adjust_prev = make_likert('I adapted my strategy based on others’ previous actions.') beh_fair_over_payoff = make_likert('I preferred fair outcomes even if they reduced my payoff.') beh_align_partner = make_likert('I preferred to cooperate repeatedly with the same partner.') beh_watch_reactions = make_likert('I watched others’ reactions before confirming an agreement.') # ===================== Perception (AI-neutral) ===================== perc_coop_over_comp = make_likert('The game felt more cooperative than competitive.') perc_understanding_over_calc = make_likert('Reaching an agreement required mutual understanding, not just calculation.') perc_exclude_pressure = make_likert('I sometimes felt pressure to exclude someone.') perc_all_three_easy = make_likert('It was easy to find outcomes that satisfied everyone.') # ===================== AI-specific Behaviour (only if has_ai) ===================== beh_checked_label = make_likert('I was aware of my opponents’ identities (Human/AI) throughout the entire game.') beh_delegate_future = make_likert( 'I would be willing to delegate negotiations in a similar game to an AI agent in the future.' ) beh_changed_by_label = make_likert('I changed my strategy based on whether the player was AI or human.') # ===================== AI vs Human comparative (choices randomized per participant) ===================== # NOTE: we do NOT set static choices here; they are provided dynamically by _choices(player) ai_your_fav = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='My Favorite player to agree with') ai_fairer = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='Fairer overall') ai_trustworthy = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='More trustworthy') ai_predictable = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='More predictable') ai_easier_coop = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='Harder to cooperate with') ai_more_strategic = models.IntegerField(widget=widgets.RadioSelectHorizontal, label='More strategic / calculating') def _ai_first(player: Player) -> bool: # Pure getter. Orientation was set in creating_session. return player.participant.vars.get('ai_first', True) def _ai_human_bipolar_choices(player: 'Player'): """ 7-point bipolar scale with 'Same for both' at the midpoint. Randomly flips which side shows 'AI' vs 'Human', per participant. """ if _ai_first(player): # AI on the left, Human on the right return [ [1, "AI"], [2, ""], [3, ""], [4, "Same for both"], [5, ""], [6, ""], [7, "Human"], ] else: # Human on the left, AI on the right return [ [1, "Human"], [2, ""], [3, ""], [4, "Same for both"], [5, ""], [6, ""], [7, "AI"], ] # ---- dynamic choice functions for AISymmetric fields (oTree auto-detects these) ---- def ai_your_fav_choices(player): return _ai_human_bipolar_choices(player) def ai_fairer_choices(player): return _ai_human_bipolar_choices(player) def ai_trustworthy_choices(player): return _ai_human_bipolar_choices(player) def ai_predictable_choices(player): return _ai_human_bipolar_choices(player) def ai_easier_coop_choices(player): return _ai_human_bipolar_choices(player) def ai_more_strategic_choices(player): return _ai_human_bipolar_choices(player) # ===================== PAGES ===================== class Survey(Page): form_model = 'player' # General, AI-neutral: behaviour + perception (matrix table) form_fields = [ # Behaviour (self-report) 'beh_first_proposal', 'beh_counter_proposal', 'beh_strategy_adjust_prev', 'beh_fair_over_payoff', 'beh_align_partner', 'beh_watch_reactions', # Perception 'perc_coop_over_comp', 'perc_understanding_over_calc', 'perc_exclude_pressure', 'perc_all_three_easy', ] timeout_seconds = 240 # use your own if needed class SurveyAI(Page): form_model = 'player' # AI-specific behaviour only (matrix table, same Agree↔Disagree header) form_fields = [ 'beh_checked_label', 'beh_delegate_future', 'beh_changed_by_label', ] timeout_seconds = 90 @staticmethod def is_displayed(player: Player): n_ai = player.participant.vars.get('n_ai_opponents', 0) # Only show for participants with 1+ AI in group return n_ai > 0 class AISymmetric(Page): form_model = 'player' # AI vs Human comparative (matrix table with AI↔Human header), choices randomized per participant form_fields = [ 'ai_your_fav', 'ai_fairer', 'ai_trustworthy', 'ai_predictable', 'ai_easier_coop', 'ai_more_strategic', ] timeout_seconds = 180 @staticmethod def is_displayed(player: Player): n_ai = player.participant.vars.get('n_ai_opponents', 0) # Only show for humans who had exactly one AI opponent return n_ai == 1 # Show AI parts first (they auto-skip when not applicable), then the general page page_sequence = [SurveyAI, AISymmetric, Survey]