from otree.api import * c = cu doc = 'Disruption Game' class C(BaseConstants): NAME_IN_URL = 'DisruptionGame' PLAYERS_PER_GROUP = None NUM_ROUNDS = 24 LOW_VAR_ONE_DIS_DEMAND = (486, 625, 512, 453, 503, 484, 505, 444, 465, 549, 479, 555, 490, 592, 463, 508, 447, 500, 359, 493, 616, 448, 585, 530) LOW_VAR_TWO_DIS_DEMAND = (486, 625, 512, 453, 504, 484, 511, 389, 430, 598, 457, 610, 490, 592, 463, 508, 447, 500, 359, 493, 616, 448, 585, 530) RETAIL_COST = 15 WHOLESALE_COST = 6 HOLDING_COST = 1 HIGH_VAR_ONE_DIS_DEMAND = (472, 750, 524, 407, 508, 468, 511, 389, 430, 598, 457, 610, 479, 684, 426, 517, 393, 500, 219, 487, 732, 397, 670, 560) HIGH_VAR_TWO_DIS_DEMAND = (472, 750, 524, 407, 508, 468, 521, 277, 360, 696, 415, 719, 479, 684, 426, 517, 393, 500, 219, 487, 732, 397, 670, 560) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): from random import choices import random # Clear previous image and demand data if they exist for player in subsession.get_players(): player.participant.vars['demand_image'] = None # Clear any previous image player.participant.vars['current_demand_std'] = None # Clear any previous std deviation # Define treatments treatments = ['LowVarOneDis', 'LowVarTwoDis', 'HighVarOneDis', 'HighVarTwoDis'] weights = [0, 0, 0, 1] for player in subsession.get_players(): print(f"Participant {player.participant.id_in_session}: Treatment in Second App = {player.participant.vars.get('treatment', 'Not Set')}") # Assign treatment randomly if not player.treatment: assigned_treatment = choices(treatments, weights=weights, k=1)[0] player.participant.vars['treatment'] = assigned_treatment player.treatment = player.participant.vars['treatment'] if not player.payment_round: payment_round = random.choice([1, 3, 4, 5, 6]) player.participant.vars['payment_round'] = payment_round player.payment_round = payment_round # Initialize participant-level variables player.participant.vars['cumulative_score'] = 0 # Running score across rounds # Set demand list and initial std deviation based on treatment if assigned_treatment == 'LowVarOneDis': player.participant.vars['demand_list'] = C.LOW_VAR_ONE_DIS_DEMAND player.participant.vars['initial_demand_std'] = 50 player.participant.vars['demand_image'] = '50STD.png' elif assigned_treatment == 'LowVarTwoDis': player.participant.vars['demand_list'] = C.LOW_VAR_TWO_DIS_DEMAND player.participant.vars['initial_demand_std'] = 50 player.participant.vars['demand_image'] = '50STD.png' elif assigned_treatment == 'HighVarOneDis': player.participant.vars['demand_list'] = C.HIGH_VAR_ONE_DIS_DEMAND player.participant.vars['initial_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' elif assigned_treatment == 'HighVarTwoDis': player.participant.vars['demand_list'] = C.HIGH_VAR_TWO_DIS_DEMAND player.participant.vars['initial_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' # Set initial demand mean and current demand standard deviation player.participant.vars['initial_demand_mean'] = 500 player.participant.vars['current_demand_std'] = player.participant.vars['initial_demand_std'] # Set disruption timing based on treatment if assigned_treatment in ['LowVarOneDis', 'HighVarOneDis']: # Disruption only in Round 2 player.participant.vars['disruption_round_1'] = False player.participant.vars['disruption_round_2'] = True else: # Disruption in the middle of Round 1 and in Round 2 player.participant.vars['disruption_round_1'] = True player.participant.vars['disruption_round_2'] = True # Log the assigned treatment for debugging print(f"Assigned treatment for participant {player.id_in_group}: {assigned_treatment}") print(f"Disruption settings - Round 1: {player.participant.vars['disruption_round_1']}, " f"Round 2: {player.participant.vars['disruption_round_2']}, " f"Initial variance: {player.participant.vars['initial_demand_std']}") 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() payment_amount = models.CurrencyField() payment_round = models.IntegerField() treatment = models.StringField() total_payoff = models.CurrencyField() end_time = models.StringField() 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 in [1, 13]: player.starting_inventory = 0 # Start of a new game or round else: player.starting_inventory = player.in_round(player.round_number - 1).ending_inventory # Calculate display round number display_round = player.round_number if player.round_number <= 12 else player.round_number - 12 # Initialize demand attributes if this is the first round of the game or round if player.round_number in [1, 13]: treatment = player.participant.vars['treatment'] # Set demand list and initial standard deviation/image based on treatment if treatment == 'LowVarOneDis': player.participant.vars['demand_list'] = C.LOW_VAR_DEMAND player.participant.vars['current_demand_std'] = 50 player.participant.vars['demand_image'] = '50STD.png' elif treatment == 'LowVarTwoDis': player.participant.vars['demand_list'] = C.LOW_VAR_TWO_DIS_DEMAND player.participant.vars['current_demand_std'] = 50 player.participant.vars['demand_image'] = '50STD.png' elif treatment == 'HighVarOneDis': player.participant.vars['demand_list'] = C.HIGH_VAR_ONE_DIS_DEMAND player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' elif treatment == 'HighVarTwoDis': player.participant.vars['demand_list'] = C.HIGH_VAR_TWO_DIS_DEMAND player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' # Set dynamically updated demand image and standard deviation demand_image = player.participant.vars['demand_image'] demand_image_path = f"my_folder/{demand_image}" current_demand_std = player.participant.vars['current_demand_std'] # Retrieve game history data game_history = [] for data in player.participant.vars.get('game_history', []): # Copy each entry to avoid modifying the original period_data = data.copy() if period_data['period'] == display_round: period_data['cumulative_profit'] = '' # Show blank cumulative profit for the current period game_history.append(period_data) # Retrieve values from participant.vars selected_decision = player.participant.vars.get('selected_decision', "Not applicable") mpl_payoff = player.participant.vars.get('mpl_payoff', 0) print(player.participant.vars) return { 'starting_inventory': player.starting_inventory, 'current_demand': player.du, 'display_round': display_round, 'demand_image': demand_image, # Pass only the image file name 'current_demand_std': current_demand_std, 'demand_image_path': demand_image_path, 'treatment': player.participant.vars['treatment'], 'game_history': game_history, 'selected_decision': selected_decision, 'mpl_payoff': mpl_payoff, } @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 player.profit = (units_sold * C.RETAIL_COST) - (order_quantity * C.WHOLESALE_COST) - (player.ending_inventory * C.HOLDING_COST) participant.vars['cumulative_profit'] += player.profit # Update cumulative profit after calculating profit # Prepare period data for the current round period_display = player.round_number if player.round_number <= 12 else player.round_number - 12 period_data = { 'period': period_display, 'starting_inventory': player.starting_inventory, 'order_quantity': player.order_quantity, 'demand_quantity': player.du, 'profit': player.profit, 'cumulative_profit': participant.vars['cumulative_profit'] if player.round_number > 1 else '' } # Ensure game_history is initialized if 'game_history' not in participant.vars: participant.vars['game_history'] = [] # Initialize history lists if not present if 'round_1_history' not in participant.vars: participant.vars['round_1_history'] = [] if 'round_2_history' not in participant.vars: participant.vars['round_2_history'] = [] # Append period data to the correct history list based on round if player.round_number <= 12: participant.vars['round_1_history'].append(period_data) else: participant.vars['round_2_history'].append(period_data) # 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) # Reset starting inventory and cumulative profit at the start of Round 2 if player.round_number == 12: player.starting_inventory = 0 participant.vars['cumulative_profit'] = 0 # Add a placeholder for the next period’s starting inventory next_period_display = period_display + 1 if 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) # Debug logging to confirm the game history print(f"Updated game history for round {player.round_number}: {participant.vars['game_history']}") # Update the demand standard deviation and image based on treatment and disruptions treatment = player.participant.vars['treatment'] if player.round_number in [1, 12]: if treatment in ['LowVarOneDis', 'LowVarTwoDis']: player.participant.vars['current_demand_std'] = 50 player.participant.vars['demand_image'] = '50STD.png' elif treatment in ['HighVarOneDis', 'HighVarTwoDis']: player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' # Continue with the rest of your logic for handling other disruptions, if any # For example, increase variance in later rounds based on specific treatment if treatment == 'LowVarTwoDis' and player.round_number == 6: player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' elif treatment in ['LowVarOneDis', 'LowVarTwoDis'] and player.round_number == 18: player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' elif treatment == 'HighVarTwoDis' and player.round_number == 6: player.participant.vars['current_demand_std'] = 200 player.participant.vars['demand_image'] = '200STD.png' elif treatment in ['HighVarOneDis', 'HighVarTwoDis'] and player.round_number == 18: player.participant.vars['current_demand_std'] = 200 player.participant.vars['demand_image'] = '200STD.png' class Results(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): # Calculate display round number: for rounds 1-12 show as 1-12, for rounds 13-24 show as 1-12 again display_round = player.round_number if player.round_number <= 12 else player.round_number - 12 display_game = 1 if player.round_number <= 12 else 2 return { 'display_round': display_round, '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, 'display_game': display_game } @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) # Optionally, log information for debugging print(f"Period {player.round_number}: Starting inventory={player.starting_inventory}, " f"Order quantity={player.order_quantity}, Demand={player.du}, " f"Ending inventory={player.ending_inventory}") if player.round_number == 24: # or the final round of your game # Get the chosen payment round payment_round = player.participant.vars['payment_round'] # Retrieve the profit from that round payment_round_profit = player.in_round(payment_round).profit # Calculate the final payment payment_amount = payment_round_profit * 0.001 # or whatever percentage need # Store it in participant vars for later reference player.participant.vars['payment_amount'] = payment_amount print(f"End of Round 24, game_history: {participant.vars['game_history']}") class DisruptionNotice(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): # Get the participant's treatment treatment = player.participant.vars.get('treatment') # Check conditions for displaying the page # Display for treatment 'LowVarTwoDis' and 'HighVarTwoDis' in the middle of Round 1 (e.g., Period 6) if treatment in ['LowVarTwoDis', 'HighVarTwoDis'] and player.round_number == 6: return True # Display for both treatments in the middle of Round 2 if player.round_number == 18: return True return False @staticmethod def vars_for_template(player: Player): treatment = participant.vars['treatment'] # Choose image based on treatment if treatment in ['HighVarOneDis', 'HighVarTwoDis']: disruption_image_path = "my_folder/HighVarDisruption.jpg" else: disruption_image_path = "my_folder/LowVarDisruption.jpg" return { 'disruption_image_path': disruption_image_path, } @staticmethod def before_next_page(player: Player, timeout_happened): # Check the participant's treatment treatment = player.participant.vars['treatment'] # Adjust standard deviation based on treatment and round if treatment in ['LowVarTwoDis', 'HighVarTwoDis']: # LowVarTwoDis and HighVarTwoDis increase standard deviation at both disruptions if player.round_number == 6: player.participant.vars['current_demand_std'] = 100 if treatment == 'LowVarTwoDis' else 200 elif player.round_number == 18: player.participant.vars['current_demand_std'] = 100 if treatment == 'LowVarTwoDis' else 200 else: # LowVarOneDis and HighVarOneDis increase standard deviation only in round 18 if player.round_number == 18: player.participant.vars['current_demand_std'] = 100 if treatment == 'LowVarOneDis' else 200 # Debug print to verify changes print(f"Round {player.round_number}: Treatment {treatment} - Standard deviation set to {player.participant.vars['current_demand_std']}") class RoundSummary(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number in [12,24]: return True return False @staticmethod def vars_for_template(player: Player): # Initialize round_number to ensure it's always defined round_number = 1 if player.round_number <= 12 else 2 # Determine which slice of game history to display based on the round if round_number == 1: display_game_history = participant.vars.get('round_1_history', []) else: # For Round 2, display `round_2_history` and label periods 1-12 display_game_history = [ {**period_data, 'period': period_data['period']} for period_data in participant.vars.get('round_2_history', []) ] # Check if it's the end of Round 1 (in this case, period 12) show_reset_message = player.round_number == 12 return { 'display_game_history': display_game_history, 'round_number': round_number, 'show_reset_message': show_reset_message } @staticmethod def before_next_page(player: Player, timeout_happened): from datetime import datetime if player.round_number in [12]: participant.vars['game_history'] = [] player.payment_amount = player.participant.vars.get('payment_amount', 0) player.payment_round = player.participant.vars['payment_round'] player.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") class Final_Results(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): if player.round_number in [24]: return True return False @staticmethod def vars_for_template(player: Player): print(player.participant.vars) # Retrieve the payment amount from participant vars payment_amount = player.participant.vars.get('payment_amount', 0) payment_round = player.participant.vars['payment_round'] selected_decision = player.participant.vars.get('selected_decision') mpl_payoff = player.participant.vars.get('mpl_payoff') # Retrieve values from participant.vars total_payoff = player.payment_amount + 1.50 + mpl_payoff return { 'payment_amount': payment_amount, 'payment_round': payment_round, 'selected_decision': selected_decision, 'mpl_payoff': mpl_payoff, 'total_payoff': total_payoff, } @staticmethod def before_next_page(player: Player, timeout_happened): from datetime import datetime # Retrieve values from participant.vars mpl_payoff = player.participant.vars.get('mpl_payoff', 0) payment_amount = player.participant.vars.get('payment_amount', 0) # Calculate and save total payoff player.total_payoff = payment_amount + mpl_payoff + 1.50 player.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") page_sequence = [Decision, Results, DisruptionNotice, RoundSummary, Final_Results]