from otree.api import * c = cu doc = '' class C(BaseConstants): NAME_IN_URL = 'experiment_t2_e2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 30 LEFT_ARM_PRIZE = 20 RIGHT_ARM_PRIZE = 50 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): upgrade_bought = models.StringField() bandit_choice = models.StringField() bandit_is_win = models.BooleanField() bandit_earnings = models.IntegerField() hp_lost = models.IntegerField(initial=0) attention_passed2 = models.BooleanField() is_winner = models.BooleanField() is_optimal = models.BooleanField() task_engagement = models.IntegerField(choices=[[1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6'], [7, '7']], widget=widgets.RadioSelectHorizontal) total_bank = models.IntegerField(initial=0) pocket_money = models.IntegerField(initial=0) spent_credits = models.IntegerField() decision_rt = models.FloatField(blank=True) p2_q1_rules = models.IntegerField() p2_q2_tickets = models.IntegerField() p2_q4_engaging = models.IntegerField() p2_q6_focused = models.IntegerField() p2_q7_repetitive = models.IntegerField() p2_q9_identify_best = models.IntegerField() game_familiarity = models.IntegerField() exp_familiarity = models.IntegerField() age = models.IntegerField() gender = models.StringField() field_of_study = models.StringField(blank=True) study_level = models.StringField() coursework = models.StringField() prev_econ_exp = models.StringField() tech_issues = models.LongStringField(blank=True) enter_draw = models.StringField() email = models.StringField(blank=True) video_game_freq = models.StringField() p2_cq1 = models.StringField(choices=[['Fewer coins are saved in the Bank for the Phase 2 prize draw', 'Fewer coins are saved in the Bank for the Phase 2 prize draw'], ['More coins are saved in the Bank for the Phase 2 prize draw', 'More coins are saved in the Bank for the Phase 2 prize draw'], ['It makes no difference to the number of coins saved in the Bank,', 'It makes no difference to the number of coins saved in the Bank'], ['I am not sure', 'I am not sure']], label='1. Buying a weapon upgrade in Phase 2 means:', widget=widgets.RadioSelect) p2_cq_errors = models.IntegerField(initial=0) p2_cq2 = models.StringField(choices=[['Your Bank balance is reduced', 'Your Bank balance is reduced'], ['Your Bank balance is unchanged', 'Your Bank balance is unchanged'], ['Your sector reward probabilities will change', 'Your sector reward probabilities will change'], ['I am not sure', 'I am not sure']], label='2. If you lose HP or fail the mini-game, this means:', widget=widgets.RadioSelect) p2_cq3 = models.StringField(choices=[['The same as in Phase 1', 'The same as in Phase 1'], ['Different from Phase 1', 'Different from Phase 1'], ['Chosen by the participant', 'Chosen by the participant'], ['I am not sure', 'I am not sure']], label='3. In Phase 2, the sector reward patterns are:', widget=widgets.RadioSelect) battle_skipped = models.BooleanField(initial=False) p2_belief_alpha = models.IntegerField(max=100, min=0) p2_belief_beta = models.IntegerField(max=100, min=0) p2_belief_gamma = models.IntegerField(max=100, min=0) comments = models.LongStringField(blank=True) monthly_budget = models.StringField(blank=True) p2_cq4 = models.StringField() class Instruction(Page): form_model = 'player' form_fields = ['p2_cq2', 'p2_cq_errors', 'p2_cq4'] @staticmethod def is_displayed(player: Player): return player.participant.vars.get('group') == 'Group 1' and player.round_number == 1 class BanditPage(Page): form_model = 'player' form_fields = ['bandit_choice', 'decision_rt'] @staticmethod def is_displayed(player: Player): return player.participant.vars.get('group') == 'Group 1' @staticmethod def vars_for_template(player: Player): last_earned = None last_choice_name = None if player.round_number > 1: prev_player = player.in_round(player.round_number - 1) if prev_player.bandit_earnings is not None: last_earned = prev_player.bandit_earnings if prev_player.bandit_choice: choice_str = prev_player.bandit_choice.lower() if choice_str == 'left': last_choice_name = "SECTOR ALPHA" elif choice_str == 'mid': last_choice_name = "SECTOR BETA" elif choice_str == 'right': last_choice_name = "SECTOR GAMMA" else: last_choice_name = "UNKNOWN SECTOR" return { 'last_earned': last_earned, 'last_choice_name': last_choice_name } @staticmethod def before_next_page(player: Player, timeout_happened): import random if player.bandit_choice: my_choice = player.bandit_choice.lower() p_left, prize_left = 0.5, 22 p_mid, prize_mid = 0.35, 28 p_right, prize_right = 0.65, 15 if my_choice == 'left': prob = p_left actual_prize = prize_left elif my_choice == 'mid': prob = p_mid actual_prize = prize_mid else: # 'right' prob = p_right actual_prize = prize_right ev_left = p_left * prize_left ev_mid = p_mid * prize_mid ev_right = p_right * prize_right max_ev = max(ev_left, ev_mid, ev_right) if my_choice == 'left' and ev_left == max_ev: player.is_optimal = True elif my_choice == 'mid' and ev_mid == max_ev: player.is_optimal = True elif my_choice == 'right' and ev_right == max_ev: player.is_optimal = True else: player.is_optimal = False if random.random() < prob: player.is_winner = True player.payoff = actual_prize player.bandit_earnings = actual_prize else: player.is_winner = False player.payoff = 0 player.bandit_earnings = 0 class Encounter(Page): form_model = 'player' form_fields = ['upgrade_bought', 'hp_lost', 'spent_credits'] @staticmethod def is_displayed(player: Player): return player.participant.vars.get('group') == 'Group 1' @staticmethod def vars_for_template(player: Player): SCENARIOS = [ # ── ACT 1: TRAINING (Rounds 1–9) ── # Short and easy. Learn the controls. 4-5 seconds each. {"cost": 3, "weapon": "Dual Laser", "w_type": 1, "enemies": "basic", "is_boss": False, "time": 4}, # R1 {"cost": 3, "weapon": "Spread Gun", "w_type": 2, "enemies": "basic", "is_boss": False, "time": 4}, # R2 {"cost": 3, "weapon": "Plasma Beam", "w_type": 3, "enemies": "fast", "is_boss": False, "time": 5}, # R3 {"cost": 4, "weapon": "Dual Laser", "w_type": 1, "enemies": "sine", "is_boss": False, "time": 5}, # R4 {"cost": 4, "weapon": "Wave Cannon", "w_type": 4, "enemies": "basic,fast", "is_boss": False, "time": 5}, # R5 {"cost": 4, "weapon": "Homing Missile", "w_type": 5, "enemies": "fast,sine", "is_boss": False, "time": 5}, # R6 {"cost": 5, "weapon": "Ricochet Bolt", "w_type": 6, "enemies": "tank", "is_boss": False, "time": 5}, # R7 {"cost": 5, "weapon": "Spread Gun", "w_type": 2, "enemies": "sine,fast", "is_boss": False, "time": 5}, # R8 {"cost": 5, "weapon": "Plasma Beam", "w_type": 3, "enemies": "basic,tank", "is_boss": False, "time": 5}, # R9 # ── BOSS 1 (Round 10) ── {"cost": 6, "weapon": "Wave Cannon", "w_type": 4, "enemies": "basic,fast", "is_boss": True, "time": 7}, # R10 # ── ACT 2: ESCALATION (Rounds 11–19) ── # Introduce splitter. Keep stealth rare. 5-6 seconds. {"cost": 5, "weapon": "Dual Laser", "w_type": 1, "enemies": "fast,sine", "is_boss": False, "time": 5}, # R11 {"cost": 5, "weapon": "Homing Missile", "w_type": 5, "enemies": "basic,tank", "is_boss": False, "time": 5}, # R12 {"cost": 5, "weapon": "Spread Gun", "w_type": 2, "enemies": "tank,fast", "is_boss": False, "time": 6}, # R13 {"cost": 6, "weapon": "Plasma Beam", "w_type": 3, "enemies": "sine,basic", "is_boss": False, "time": 6}, # R14 {"cost": 6, "weapon": "Ricochet Bolt", "w_type": 6, "enemies": "fast,splitter", "is_boss": False, "time": 6}, # R15 {"cost": 6, "weapon": "Wave Cannon", "w_type": 4, "enemies": "tank,sine", "is_boss": False, "time": 6}, # R16 {"cost": 7, "weapon": "Dual Laser", "w_type": 1, "enemies": "fast,tank", "is_boss": False, "time": 6}, # R17 {"cost": 7, "weapon": "Homing Missile", "w_type": 5, "enemies": "sine,splitter", "is_boss": False, "time": 6}, # R18 {"cost": 7, "weapon": "Spread Gun", "w_type": 2, "enemies": "fast,sine", "is_boss": False, "time": 6}, # R19 # ── BOSS 2 (Round 20) ── {"cost": 7, "weapon": "Plasma Beam", "w_type": 3, "enemies": "fast,tank", "is_boss": True, "time": 8}, # R20 # ── ACT 3: PEAK (Rounds 21–29) ── # No splitter or stealth here — feedback says they're frustrating. # Use fast, sine, tank combos which are challenging but fair. 5-7 seconds. {"cost": 6, "weapon": "Wave Cannon", "w_type": 4, "enemies": "fast,sine", "is_boss": False, "time": 5}, # R21 {"cost": 6, "weapon": "Ricochet Bolt", "w_type": 6, "enemies": "tank,fast", "is_boss": False, "time": 6}, # R22 {"cost": 7, "weapon": "Homing Missile", "w_type": 5, "enemies": "sine,tank", "is_boss": False, "time": 6}, # R23 {"cost": 7, "weapon": "Dual Laser", "w_type": 1, "enemies": "fast,sine,tank", "is_boss": False, "time": 6}, # R24 {"cost": 7, "weapon": "Spread Gun", "w_type": 2, "enemies": "fast,tank", "is_boss": False, "time": 6}, # R25 {"cost": 8, "weapon": "Plasma Beam", "w_type": 3, "enemies": "sine,fast", "is_boss": False, "time": 7}, # R26 {"cost": 8, "weapon": "Wave Cannon", "w_type": 4, "enemies": "tank,sine", "is_boss": False, "time": 7}, # R27 {"cost": 8, "weapon": "Ricochet Bolt", "w_type": 6, "enemies": "fast,tank", "is_boss": False, "time": 7}, # R28 {"cost": 8, "weapon": "Homing Missile", "w_type": 5, "enemies": "sine,fast,tank", "is_boss": False, "time": 7}, # R29 # ── FINAL BOSS (Round 30) ── {"cost": 8, "weapon": "Wave Cannon", "w_type": 4, "enemies": "fast,tank,sine", "is_boss": True, "time": 8}, # R30 ] if player.round_number == 1: starting_bank = 0 else: prev_player = player.in_round(player.round_number - 1) starting_bank = prev_player.total_bank if prev_player.total_bank is not None else 0 pocket = player.bandit_earnings if player.bandit_earnings is not None else 0 idx = min(player.round_number - 1, len(SCENARIOS) - 1) sc = SCENARIOS[idx] return { 'starting_bank': starting_bank, 'pocket_money': pocket, 'upgrade_cost': sc['cost'], 'weapon_name': sc['weapon'], 'weapon_type': sc['w_type'], 'enemy_types': sc['enemies'], 'is_boss': 'True' if sc['is_boss'] else 'False', 'time_limit': sc['time'], 'round': player.round_number, } @staticmethod def before_next_page(player: Player, timeout_happened): if player.round_number == 1: starting_bank = 0 else: prev_player = player.in_round(player.round_number - 1) starting_bank = prev_player.total_bank if prev_player.total_bank is not None else 0 pocket = player.bandit_earnings if player.bandit_earnings is not None else 0 spt = player.spent_credits if player.spent_credits is not None else 0 final_bank = starting_bank + pocket - spt if final_bank < 0: final_bank = 0 player.total_bank = final_bank player.pocket_money = pocket class Result(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): return player.participant.vars.get('group') == 'Group 1' and player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): past_players = player.in_all_rounds() earned = sum([p.bandit_earnings for p in past_players if p.bandit_earnings is not None]) spent = sum([p.spent_credits for p in past_players if p.spent_credits is not None]) final_wealth = earned - spent if final_wealth < 0: final_wealth = 0 player.participant.vars['task2_total_bank'] = final_wealth return {'final_tickets': final_wealth} class Questionnaire(Page): form_model = 'player' form_fields = ['p2_q1_rules', 'p2_q2_tickets', 'p2_q4_engaging', 'p2_q6_focused', 'p2_q7_repetitive', 'p2_q9_identify_best', 'age', 'gender', 'field_of_study', 'study_level', 'coursework', 'prev_econ_exp', 'tech_issues', 'video_game_freq', 'p2_belief_alpha', 'p2_belief_beta', 'p2_belief_gamma', 'comments', 'monthly_budget'] @staticmethod def is_displayed(player: Player): return player.participant.vars.get('group') == 'Group 1' and player.round_number == C.NUM_ROUNDS page_sequence = [Instruction, BanditPage, Encounter, Result, Questionnaire]