import random from otree.api import Bot, Submission, expect from . import ( C, APP_CONFIG, Captcha, Welcome, Consent, AnnounceApps, TransitionPage, Application, YourTask, CQs, IntroduceMPL, SuggestConsiderations, Cases, Cases2, Cases3Explained, Cases3, ReviewStatements, RedoCases, RedoCases2, RedoCases3Explained, RedoCases3, RedoReviewStatements, MotivesOpen, IdealsProjection, WTPSummary, MotivesInconsistency, DougsDG, PolicyQuestions, ADStatements, Feedback, Redirect, ) # Focus tracking fields that every page requires (injected by _add_focus_tracking) FOCUS_FIELDS = dict( _focus_blur_count=0, _focus_blur_duration=0.0, _focus_vis_count=0, _focus_vis_duration=0.0, ) def sub(page_cls, data=None): """Shorthand for Submission with check_html=False (focus fields aren't in static HTML).""" if data is None: data = dict(**FOCUS_FIELDS) return Submission(page_cls, data, check_html=False) def get_correct_cqs(player): """Return correct CQ values for the current round's app and treatment.""" round_idx = player.round_number - 1 app_index = player.participant.app_order[round_idx] app_name = C.APP_NAMES[app_index] config = APP_CONFIG[app_name] cqs = config['cqs'] values = {} for i in range(1, 6): key = f'cq{i}' values[key] = cqs[key]['correct'] + 1 # 0-indexed -> 1-indexed treatment = player.participant.treatment if treatment == 'ambiguous': values['cq6_ambiguous'] = cqs['cq6_ambiguous']['correct'] + 1 else: if treatment == 'high': values['cq6_treatments'] = 1 else: values['cq6_treatments'] = 2 return values class PlayerBot(Bot): def play_round(self): rn = self.round_number participant = self.participant # ================================================================ # PRE-LOOP (round 1 only) # ================================================================ if rn == 1: yield sub(Captcha, dict(recaptcha_token='bot_test_token', **FOCUS_FIELDS)) yield sub(Welcome, dict(browser='Bot/1.0', **FOCUS_FIELDS)) yield sub(Consent) yield sub(AnnounceApps) # ================================================================ # LOOP (every round) # ================================================================ # TransitionPage (only round > 1) if rn > 1: yield sub(TransitionPage) # Application (info page) yield sub(Application) # YourTask (info page) yield sub(YourTask) # CQs - must provide correct answers cq_values = get_correct_cqs(self.player) yield sub(CQs, dict(**cq_values, **FOCUS_FIELDS)) # IntroduceMPL (info page) yield sub(IntroduceMPL) # SuggestConsiderations (info page) yield sub(SuggestConsiderations) # ---------------------------------------------------------------- # Elicitation choices: vary bot behavior for interesting data # ---------------------------------------------------------------- bot_id = self.player.id_in_group # 1-based # Vary bot profiles to produce different WTP patterns profile = (bot_id + rn) % 4 if profile == 0: # Profile A: prefers original in both cases (ES pattern) trad_choice = 1 # original es_choice = 1 # original elif profile == 1: # Profile B: prefers original learns, indifferent doesn't learn (MS) trad_choice = 1 # original es_choice = 3 # indifferent elif profile == 2: # Profile C: prefers original both, but different WTP (ES+MS) trad_choice = 1 # original es_choice = 1 # original else: # Profile D: mix - sometimes fake, sometimes original trad_choice = random.choice([1, 1, 2]) es_choice = random.choice([1, 3, 3]) # Cases (Step 1): binary choice + comprehension checks yield sub(Cases, dict( Trad_wtp=trad_choice, ES_wtp=es_choice, Trad_learn=1, # correct: John WILL learn ES_learn=2, # correct: John will NOT learn Trad_learn_mistakes=0, ES_learn_mistakes=0, **FOCUS_FIELDS, )) # Cases2 (Step 2): +$1 choice cases2_data = dict( Trad_learn2_mistakes=0, ES_learn2_mistakes=0, **FOCUS_FIELDS, ) # Only fill in fields for cases that weren't indifferent in Step 1 if trad_choice != 3: cases2_data['Trad_learn2'] = 1 # correct cases2_data['Trad_wtp2'] = 1 # still prefer original if es_choice != 3: cases2_data['ES_learn2'] = 2 # correct if profile == 2: # For ES+MS profile, switch to indifferent at Step 2 cases2_data['ES_wtp2'] = 3 # indifferent else: cases2_data['ES_wtp2'] = 1 # still prefer original yield sub(Cases2, cases2_data) # Determine if Cases3Explained will be displayed (Cases3 is always displayed) trad_wtp2 = cases2_data.get('Trad_wtp2') es_wtp2 = cases2_data.get('ES_wtp2') trad_strict = (trad_choice == 1 and trad_wtp2 == 1) es_strict = (es_choice == 1 and es_wtp2 == 1) # Cases3Explained: only shown if at least one case is strict if trad_strict or es_strict: yield sub(Cases3Explained) # Cases3: always displayed (non-strict cases are handled in the template) cases3_data = dict( Trad_learn3_mistakes=0, ES_learn3_mistakes=0, **FOCUS_FIELDS, ) if trad_strict: cases3_data['Trad_learn3'] = 1 # correct # Pick a switch point across the full MPL range (0-14, or 15 = never switch) # Weight toward higher rows so WTP values span the full $2-$201 range trad_mpl = str(random.choice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])) cases3_data['Trad_wtp3'] = trad_mpl if es_strict: cases3_data['ES_learn3'] = 2 # correct # For ES pattern (profile 0): same WTP as Trad if profile == 0 and trad_strict: es_mpl = trad_mpl else: es_mpl = str(random.choice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])) cases3_data['ES_wtp3'] = es_mpl yield sub(Cases3, cases3_data) # ReviewStatements: confirm choices yield sub(ReviewStatements, dict( confirm=1, # Yes, confirm timeSpentReview=5.0, **FOCUS_FIELDS, )) # Redo pages: never shown because we confirmed (confirm=1) # MotivesOpen motives = [ "I chose what I thought was best for John.", "John's well-being depends on the actual quality.", "What matters is the genuine experience.", "I focused on what John would prefer if he knew.", "I considered both the experience and the authenticity.", "My choice reflects what I value for someone else.", ] yield sub(MotivesOpen, dict( motives_open=random.choice(motives), pasted_motives_open=False, **FOCUS_FIELDS, )) # IdealsProjection yield sub(IdealsProjection, dict( ideals_projection=random.randint(2, 5), **FOCUS_FIELDS, )) # ================================================================ # POST-LOOP (round 6 only) # ================================================================ if rn == C.NUM_ROUNDS: # WTPSummary: dev mode only, won't show in production sessions if self.player.session.config.get('development'): yield sub(WTPSummary) # MotivesInconsistency: conditional on last_good & second_good # Bot profiles always produce at least 2 different categories (ES, MS, ES+MS) # so both last_good and second_good will always exist. inconsistency_reasons = [ "The scenarios felt different in terms of impact on John.", "Some situations matter more when the person knows vs doesn't.", "Authenticity matters more for some goods than others.", "In one case the deception felt more consequential.", ] yield sub(MotivesInconsistency, dict( inconsistency_reason=random.choice(inconsistency_reasons), pasted_inconsistency_reason=False, **FOCUS_FIELDS, )) # DougsDG: conditional on last_good existing (always true with our profiles) split_a = random.randint(3, 7) yield sub(DougsDG, dict( dg_split_a=split_a, dg_split_b=10 - split_a, dg_comparison="Both options have merit but I lean toward the one I allocated more to.", dg_indifferent=random.choice([True, False]), **FOCUS_FIELDS, )) # PolicyQuestions policy_options = [ 'much_better', 'slightly_better', 'neither', 'slightly_worse', 'much_worse', ] yield sub(PolicyQuestions, dict( policy_norman=random.choice(policy_options), policy_manipulation=random.choice(policy_options), policy_surveillance=random.choice(policy_options), policy_data=random.choice(policy_options), policy_counterfeit=random.choice(policy_options), policy_nozick=random.choice(policy_options), policy_healthcare=random.choice(policy_options), **FOCUS_FIELDS, )) # ADStatements yield sub(ADStatements, dict( ad_statement_1=random.randint(1, 5), ad_statement_2=random.randint(1, 5), ad_statement_3=random.randint(1, 5), ad_statement_4=random.randint(1, 5), ad_statement_5=random.randint(1, 5), **FOCUS_FIELDS, )) # Feedback yield sub(Feedback, dict( feedback="The experiment was well-designed and interesting.", pasted_feedback=False, feedbackDifficulty=random.randint(3, 7), feedbackUnderstanding=random.randint(6, 9), feedbackSatisfied=random.randint(5, 8), feedbackPay=random.randint(5, 8), **FOCUS_FIELDS, )) # Redirect page yield Submission(Redirect, check_html=False)