from otree.api import * c = cu doc = 'Disruption Game' class C(BaseConstants): NAME_IN_URL = 'Instructions' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3 PRACTICE_ROUND_DEMAND = (110, 87, 106) PRETAIL_COST1 = 30 PWHOLESALE_COST1 = 12 PHOLDING_COST1 = 2 PRETAIL_COST2 = 8 PHOLDING_COST2 = 1 PWHOLESALE_COST2 = 3 NUM_DECISIONS = 10 LOTTERY_A_HI = 2 LOTTERY_A_LO = 1.6 LOTTERY_B_HI = 3.85 LOTTERY_B_LO = 0.1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): from random import choices, choice practice_treatments = ['PracticeCost1', 'PracticeCost2'] treatments = ['LowVar', 'LowVarDis', 'HighVar'] weights = [1/3, 1/3, 1/3] for player in subsession.get_players(): assigned_practice_treatment = choice(practice_treatments) player.participant.vars['practice_treatment'] = assigned_practice_treatment if 'treatment' not in player.participant.vars: assigned_treatment = choices(treatments, weights=weights, k=1)[0] player.participant.vars['treatment'] = assigned_treatment print(f"Participant {player.participant.id_in_session}: Assigned Treatment = {player.participant.vars.get('treatment')}") # Practice cost parameters if assigned_practice_treatment == 'PracticeCost1': player.participant.vars['practice_retail_cost'] = C.PRETAIL_COST1 player.participant.vars['practice_wholesale_cost'] = C.PWHOLESALE_COST1 player.participant.vars['practice_holding_cost'] = C.PHOLDING_COST1 elif assigned_practice_treatment == 'PracticeCost2': player.participant.vars['practice_retail_cost'] = C.PRETAIL_COST2 player.participant.vars['practice_wholesale_cost'] = C.PWHOLESALE_COST2 player.participant.vars['practice_holding_cost'] = C.PHOLDING_COST2 # Practice-only variables player.participant.vars['practice_cumulative_score'] = 0 player.participant.vars['practice_demand_list'] = C.PRACTICE_ROUND_DEMAND player.participant.vars['practice_initial_demand_std'] = 10 player.participant.vars['practice_initial_demand_mean'] = 100 player.participant.vars['practice_current_demand_std'] = 10 player.participant.vars['practice_demand_image'] = 'PracticeDist.png' class Group(BaseGroup): pass class Player(BasePlayer): order_quantity = models.IntegerField(min=0) starting_inventory = models.IntegerField(initial=0) ending_inventory = models.IntegerField(initial=0) du = models.IntegerField(initial=0, label='Demand Units for this round') unmet_demand = models.IntegerField(initial=0) profit = models.CurrencyField(initial=0) cumulative_profit = models.CurrencyField(initial=0) payment = models.CurrencyField() prolificid = models.StringField(label='Please provide your prolific ID here') q1_answer = models.StringField(choices=[['A', '100 units'], ['B', '40 units'], ['C', '60 units'], ['D', '0 units']], label=' ', widget=widgets.RadioSelect) q1_correct = models.BooleanField(initial=False) q3_answer = models.StringField(choices=[['A', 'The likelihood that you will sell all your inventory in a period.'], ['B', 'The likelihood that you will have enough inventory to meet customer demand.'], ['C', 'The amount of leftover inventory carried over to the next round.'], ['D', "The difference between your profit and total holding costs.'"]], label=' ', widget=widgets.RadioSelect) q3_correct = models.BooleanField(initial=False) consent = models.BooleanField(choices=[[True, 'Yes'], [False, 'No']], label='Do you consent to participating in this study? ', widget=widgets.RadioSelect) Risk = models.StringField(choices=[['A', 'Fully unwilling to take risks'], ['B', 'Unwilling to take risks'], ['C', 'Somewhat unwilling to take'], ['D', 'Neutral'], ['E', 'Somewhat prepared to take risks'], ['F', 'Prepared to take risks'], ['G', 'Fully prepared to take risks']], label='Are you generally a person who is fully prepared to take risks or do you try to avoid taking risks?', widget=widgets.RadioSelect) selected_decision = models.IntegerField(initial=0) decision_1 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_2 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_3 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_4 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_5 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_6 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_7 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_8 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_9 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) decision_10 = models.StringField(choices=[['A', 'A'], ['B', 'B']], widget=widgets.RadioSelectHorizontal) selected_decisio = models.IntegerField() mpl_payoff = models.CurrencyField(initial=0) treatment = models.StringField() class ConsentForm(Page): form_model = 'player' form_fields = ['consent', 'prolificid'] @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class NoConsent(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): # Check if this is the first round and the participant did not give consent if player.round_number == 1: if player.consent is None: return False # Prevents errors if consent is uninitialized return not player.consent return False class Risk(Page): form_model = 'player' form_fields = ['Risk'] @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class HoltLauryPage(Page): form_model = 'player' form_fields = ['decision_1', 'decision_2', 'decision_4', 'decision_3', 'decision_5', 'decision_6', 'decision_7', 'decision_8', 'decision_9', 'decision_10'] @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False @staticmethod def vars_for_template(player: Player): return { 'decisions': [ { 'index': i + 1, # 1-based indexing for direct use in the template 'prob_fraction': f"{i + 1}/10", # Format probabilities as fractions 'lottery_a_hi': f"{C.LOTTERY_A_HI:.2f}", 'lottery_a_lo': f"{C.LOTTERY_A_LO:.2f}", 'lottery_b_hi': f"{C.LOTTERY_B_HI:.2f}", 'lottery_b_lo': f"{C.LOTTERY_B_LO:.2f}", 'css_gradient': f"conic-gradient(#4CAF50 {(i + 1) / C.NUM_DECISIONS * 360}deg, #F44336 {(i + 1) / C.NUM_DECISIONS * 360}deg 360deg)" } for i in range(C.NUM_DECISIONS) ], 'selected_decision': player.selected_decision, 'mpl_payoff': player.mpl_payoff, } @staticmethod def before_next_page(player: Player, timeout_happened): import random # Randomly select one decision (decision_1 to decision_10) selected_decision = random.randint(1, 10) player.selected_decision = selected_decision # Retrieve the player's choice for the selected decision choice = getattr(player, f'decision_{selected_decision}') # e.g., 'A' or 'B' # Determine the payoff based on the choice if choice == 'A': # Option A: Safe choice with two possible payouts high_payoff = C.LOTTERY_A_HI # Higher payoff for Option A low_payoff = C.LOTTERY_A_LO # Lower payoff for Option A probability_high = selected_decision / C.NUM_DECISIONS # Probability for higher payoff player.mpl_payoff = high_payoff if random.random() < probability_high else low_payoff elif choice == 'B': # Option B: Risky lottery high_payoff = C.LOTTERY_B_HI # Higher payoff for Option B low_payoff = C.LOTTERY_B_LO # Lower payoff for Option B probability_high = selected_decision / C.NUM_DECISIONS # Probability for higher payoff player.mpl_payoff = high_payoff if random.random() < probability_high else low_payoff # Save values to the participant object player.participant.vars['selected_decision'] = player.selected_decision player.participant.vars['mpl_payoff'] = player.mpl_payoff @staticmethod def error_message(player: Player, values): for i in range(1, 11): # Loop through all decisions if not values.get(f"decision_{i}"): return f"Please answer all decisions, including Decision {i}." class Video_Instructions(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class UnitsInventoryQuestion(Page): form_model = 'player' form_fields = ['q1_answer'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 and not player.q1_correct @staticmethod def before_next_page(player: Player, timeout_happened): correct_answer_q1 = "C" # Correct answer for units sold if player.q1_answer == correct_answer_q1: player.q1_correct = True player.participant.vars['feedback_q1'] = ( "Correct! Units sold are the minimum of available inventory and demand. " "Here, 60 (available inventory) < 100 (demand), so units sold = 60." ) else: player.q1_correct = False player.participant.vars['feedback_q1'] = ( "Incorrect. Units sold are the minimum of available inventory and demand. " "Since available inventory was 60 and demand was 100, units sold = 60." ) class FeedbackQ1(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class ServiceLevel(Page): form_model = 'player' form_fields = ['q3_answer'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 and not player.q3_correct @staticmethod def before_next_page(player: Player, timeout_happened): correct_answer_q3 = "B" # Correct answer for service level if player.q3_answer == correct_answer_q3: player.q3_correct = True player.participant.vars['feedback_q3'] = ( "Correct! Service level refers to the likelihood that you will have enough inventory to meet customer demand." ) else: player.q3_correct = False player.participant.vars['feedback_q3'] = ( "Incorrect. Service level is the likelihood that you will have enough inventory to meet customer demand, " "balancing the cost of overage and underage." ) @staticmethod def error_message(player: Player, values): if not values.get('q3_answer'): return "Please select an answer before proceeding." class FeedbackQ2(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class Instructions(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number == 1: return True return False class Decision(Page): form_model = 'player' form_fields = ['order_quantity'] @staticmethod def vars_for_template(player: Player): # Set starting inventory based on the period if player.round_number == 1: player.starting_inventory = 0 # Start of a new game else: player.starting_inventory = player.in_round(player.round_number - 1).ending_inventory # Retrieve consistent demand image and standard deviation for practice rounds current_demand_std = 10 # Fixed value for the practice game, as there’s no disruption demand_image = 'PracticeDist.png' # Static image for practice game demand_image_path = f"my_folder/{demand_image}" demand_list = player.participant.vars['demand_list'] = C.PRACTICE_ROUND_DEMAND # Prepare the game history data to display in the table game_history = [] for data in participant.vars.get('game_history', []): # Copy each entry to avoid modifying the original period_data = data.copy() # Hide cumulative profit only for the current period (last entry in game_history) if period_data['period'] == player.round_number: period_data['cumulative_profit'] = '' # Show blank cumulative profit for the current period game_history.append(period_data) return { 'starting_inventory': player.starting_inventory, 'current_demand': player.du, 'demand_image': demand_image, # Pass only the image file name 'current_demand_std': current_demand_std, 'demand_image_path': demand_image_path, 'game_history': game_history, 'demand_list': demand_list, 'practice_retail_cost': participant.vars['practice_retail_cost'], 'practice_wholesale_cost': participant.vars['practice_wholesale_cost'], 'practice_holding_cost': participant.vars['practice_holding_cost'], } @staticmethod def before_next_page(player: Player, timeout_happened): # Ensure cumulative_profit is initialized if 'cumulative_profit' not in participant.vars: participant.vars['cumulative_profit'] = 0 # Get the demand for the current round demand_list = participant.vars['demand_list'] current_demand = demand_list[player.round_number - 1] player.du = current_demand # Demand for the current round # Assign variables for demand and order quantity demand = player.du order_quantity = player.order_quantity or 0 # Default to 0 if None available_inventory = player.starting_inventory + order_quantity # Calculate units sold, leftover inventory, and unmet demand units_sold = min(demand, available_inventory) player.ending_inventory = available_inventory - units_sold player.unmet_demand = demand - units_sold # Calculate profit and update cumulative profit retail_cost = participant.vars['practice_retail_cost'] wholesale_cost = participant.vars['practice_wholesale_cost'] holding_cost = participant.vars['practice_holding_cost'] # Calculate profit player.profit = (units_sold * retail_cost) - (order_quantity * wholesale_cost) - (player.ending_inventory * holding_cost) # Prepare period data for the current round period_display = player.round_number # Should be 1-3 for practice period_data = { 'period': period_display, 'starting_inventory': player.starting_inventory, 'order_quantity': player.order_quantity, 'demand_quantity': player.du, 'profit': player.profit } # Ensure game_history is initialized if 'game_history' not in participant.vars: participant.vars['game_history'] = [] # Update game history only if it's not already recorded for this period updated = False for entry in participant.vars['game_history']: if entry['period'] == period_data['period']: entry.update(period_data) updated = True break if not updated: participant.vars['game_history'].append(period_data) # Add a placeholder for the next period’s starting inventory next_period_display = period_display + 1 if next_period_display <= 3 and not any(d['period'] == next_period_display for d in participant.vars['game_history']): next_period_data = { 'period': next_period_display, 'starting_inventory': player.ending_inventory, 'order_quantity': '', # Placeholder for order quantity 'demand_quantity': '', # Placeholder for demand quantity 'profit': '', # Placeholder for profit 'cumulative_profit': participant.vars['cumulative_profit'] } participant.vars['game_history'].append(next_period_data) # Ensure that demand image remains consistent for all practice periods (no disruptions) player.participant.vars['demand_image'] = 'PracticeDist.png' # Use a static image for the entire practice game class Results(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): return { 'demand': player.du, 'profit': player.profit, 'available_inventory': player.starting_inventory + player.order_quantity, 'starting_inventory': player.starting_inventory, 'ordered_units': player.order_quantity, 'leftover_inventory': player.ending_inventory, 'unmet_demand': player.unmet_demand, } @staticmethod def before_next_page(player: Player, timeout_happened): # Calculate ending inventory player.ending_inventory = max(0, player.starting_inventory + player.order_quantity - player.du) class RoundSummary(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number == 3: return True return False @staticmethod def vars_for_template(player: Player): # Simply display the first 3 entries in `game_history` display_game_history = participant.vars.get('game_history', [])[:3] return { 'display_game_history': display_game_history, } @staticmethod def before_next_page(player: Player, timeout_happened): # Clear any demand-related participant.vars for key in ['demand_list', 'game_history', 'round_1_history', 'round_2_history', 'round_3_history', 'cumulative_profit']: participant.vars.pop(key, None) page_sequence = [ConsentForm, NoConsent, Risk, HoltLauryPage, Video_Instructions, UnitsInventoryQuestion, FeedbackQ1, ServiceLevel, FeedbackQ2, Instructions, Decision, Results, RoundSummary]