from otree.api import * c = cu doc = 'Disruption Game' class C(BaseConstants): NAME_IN_URL = 'Disruption_Instructions_New' 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) treatment = models.StringField() Intro_low_range = models.IntegerField(label='What do you think is the lowest realized demand could be in the next round? ', min=0) Intro_high_range = models.IntegerField(label='What do you think is the highest realized demand could be in the next round? ', max=1000) 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 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 Range(Page): form_model = 'player' form_fields = ['Intro_low_range', 'Intro_high_range'] @staticmethod def is_displayed(player: Player): if player.round_number in [2]: return True return False class Decision(Page): form_model = 'player' form_fields = ['order_quantity'] @staticmethod def vars_for_template(player: Player): participant = player.participant # Set starting inventory based on the period if player.round_number == 1: player.starting_inventory = 0 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 demand_image = 'PracticeDist.png' demand_image_path = f"my_folder/{demand_image}" demand_list = C.PRACTICE_ROUND_DEMAND participant.vars['demand_list'] = demand_list # Prepare the game history data to display in the table game_history = [] for data in participant.vars.get('game_history', []): period_data = data.copy() if period_data['period'] == player.round_number: period_data['cumulative_profit'] = '' game_history.append(period_data) return { 'starting_inventory': player.starting_inventory, 'current_demand': player.du, 'demand_image': demand_image, '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): participant = player.participant # 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 # Assign variables for demand and order quantity demand = player.du order_quantity = player.order_quantity or 0 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'] player.profit = (units_sold * retail_cost) - (order_quantity * wholesale_cost) - (player.ending_inventory * holding_cost) # Update cumulative profit participant.vars['cumulative_profit'] += player.profit # Prepare period data for the current round period_display = player.round_number 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': '', 'demand_quantity': '', 'profit': '', 'cumulative_profit': participant.vars['cumulative_profit'] } participant.vars['game_history'].append(next_period_data) # Ensure demand image remains consistent participant.vars['demand_image'] = 'PracticeDist.png' 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): participant = player.participant # Existing history logic display_game_history = participant.vars.get('game_history', [])[:3] # Practice range feedback practice_range_player = player.in_round(2) practice_realized_demand = practice_range_player.du practice_lower = practice_range_player.Intro_low_range practice_upper = practice_range_player.Intro_high_range if practice_lower is not None and practice_upper is not None: practice_hit = practice_lower <= practice_realized_demand <= practice_upper else: practice_hit = False return dict( display_game_history=display_game_history, practice_realized_demand=practice_realized_demand, practice_lower=practice_lower, practice_upper=practice_upper, practice_hit=practice_hit, ) @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant # 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, Video_Instructions, UnitsInventoryQuestion, FeedbackQ1, ServiceLevel, FeedbackQ2, Instructions, Range, Decision, Results, RoundSummary]