from otree.api import * c = cu doc = 'Disruption Game' class C(BaseConstants): NAME_IN_URL = 'DisruptionGame2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 36 RETAIL_COST = 15 WHOLESALE_COST = 6 HOLDING_COST = 1 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, 598, 419, 501, 387, 561, 544, 335, 739, 426, 668, 350, 555) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): print("Running creating_session for DisruptionGame2") import random # Define treatment (only one now) TREATMENT = 'HighVarTwoDis' # Demand setup for all participants for player in subsession.get_players(): keys_to_reset = [ 'demand_list', 'game_history', 'round_1_history', 'round_2_history', 'round_3_history', 'current_demand_std', 'demand_image', 'treatment', 'payment_round', 'cumulative_profit', 'selected_decision', 'mpl_payoff' ] for key in keys_to_reset: player.participant.vars.pop(key, None) # Assign fixed treatment player.treatment = TREATMENT player.participant.vars['treatment'] = TREATMENT # Set demand parameters player.participant.vars['demand_list'] = C.HIGH_VAR_TWO_DIS_DEMAND player.participant.vars['initial_demand_std'] = 100 player.participant.vars['initial_demand_mean'] = 500 player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' # Set disruption schedule (applies to both rounds) player.participant.vars['disruption_round_1'] = True player.participant.vars['disruption_round_2'] = True player.participant.vars['disruption_round_3'] = True # Set cumulative score player.participant.vars['cumulative_score'] = 0 # Assign payment round only once 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 # Debug logging print(f"Participant {player.id_in_group}: Assigned to {TREATMENT}") print(f"Disruption in R1: {player.participant.vars['disruption_round_1']} | " f"Disruption in R2: {player.participant.vars['disruption_round_2']} | " f"Payment Round: {player.payment_round}") 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): # Fallback demand_list setup in case it's missing (e.g., after practice app) if 'demand_list' not in player.participant.vars: print("FALLBACK: Resetting demand_list and related vars in vars_for_template") player.participant.vars['demand_list'] = C.HIGH_VAR_TWO_DIS_DEMAND player.participant.vars['initial_demand_std'] = 100 player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' # Determine round number (1–3) and period (1–12) game_round = ((player.round_number - 1) // 12) + 1 display_round = ((player.round_number - 1) % 12) + 1 # Set starting inventory if player.round_number in [1, 13, 25]: player.starting_inventory = 0 else: player.starting_inventory = player.in_round(player.round_number - 1).ending_inventory # Adjust std deviation and image for volatility shifts if player.round_number in [1, 13, 25]: player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' elif player.round_number in [7, 19, 31]: player.participant.vars['current_demand_std'] = 200 player.participant.vars['demand_image'] = '200STD.png' # Display image and std 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'] # Select round history if game_round == 1: round_history = player.participant.vars.get('round_1_history', []) elif game_round == 2: round_history = player.participant.vars.get('round_2_history', []) else: round_history = player.participant.vars.get('round_3_history', []) # Create game history with only prior periods game_history = [] for data in round_history: if data['period'] < display_round: game_history.append(data.copy()) # Add a placeholder column for the current period current_period_data = { 'period': display_round, 'starting_inventory': player.starting_inventory, 'order_quantity': '', 'demand_quantity': '', 'profit': '' } game_history.append(current_period_data) return { 'starting_inventory': player.starting_inventory, 'current_demand': player.du, 'display_round': display_round, 'game_round': game_round, 'demand_image': demand_image, 'current_demand_std': current_demand_std, 'demand_image_path': demand_image_path, 'treatment': player.participant.vars['treatment'], 'game_history': game_history, 'selected_decision': player.participant.vars.get('selected_decision', "Not applicable"), 'mpl_payoff': player.participant.vars.get('mpl_payoff', 0), } @staticmethod def before_next_page(player: Player, timeout_happened): # Ensure demand_list is present — fallback in case earlier app cleared it if 'demand_list' not in participant.vars: print("FALLBACK: Resetting demand_list and related vars in before_next_page") participant.vars['demand_list'] = C.HIGH_VAR_TWO_DIS_DEMAND participant.vars['initial_demand_std'] = 100 participant.vars['initial_demand_mean'] = 500 participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' # Initialize cumulative profit if not already if 'cumulative_profit' not in participant.vars: participant.vars['cumulative_profit'] = 0 # Ensure game_history and round-specific histories are initialized participant.vars.setdefault('game_history', []) participant.vars.setdefault('round_1_history', []) participant.vars.setdefault('round_2_history', []) participant.vars.setdefault('round_3_history', []) # Get demand for the current period demand_list = participant.vars['demand_list'] current_demand = demand_list[player.round_number - 1] player.du = current_demand # Inventory and order logic order_quantity = player.order_quantity or 0 available_inventory = player.starting_inventory + order_quantity units_sold = min(current_demand, available_inventory) # Set inventory outcomes player.ending_inventory = available_inventory - units_sold player.unmet_demand = current_demand - units_sold # Profit calculation player.profit = ( units_sold * C.RETAIL_COST - order_quantity * C.WHOLESALE_COST - player.ending_inventory * C.HOLDING_COST ) participant.vars['cumulative_profit'] += player.profit # Calculate period within round (1–12) period_display = ((player.round_number - 1) % 12) + 1 # Store period data 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 '' } # Append to appropriate round history if player.round_number <= 12: participant.vars['round_1_history'].append(period_data) elif player.round_number <= 24: participant.vars['round_2_history'].append(period_data) else: participant.vars['round_3_history'].append(period_data) # Update game_history with placeholder for next period next_period_display = ((player.round_number) % 12) + 1 if not any(d['period'] == next_period_display for d in participant.vars['game_history']): participant.vars['game_history'].append({ 'period': next_period_display, 'starting_inventory': player.ending_inventory, 'order_quantity': '', 'demand_quantity': '', 'profit': '', 'cumulative_profit': participant.vars['cumulative_profit'] }) # Reset inventory and cumulative profit at the end of rounds if player.round_number in [12, 24]: player.starting_inventory = 0 participant.vars['cumulative_profit'] = 0 # Update std dev and image for volatility shocks in periods 7, 19, and 31 if player.round_number in [1, 13, 25]: # Start of each round participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' elif player.round_number in [7, 19, 31]: # Mid-round disruptions participant.vars['current_demand_std'] = 200 participant.vars['demand_image'] = '200STD.png' # Debug info print(f"Round {player.round_number}: Demand={player.du}, Profit={player.profit}") print(f"STD: {participant.vars['current_demand_std']} | Image: {participant.vars['demand_image']}") class Results(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): # Calculate display round number within current game round (1–12) display_round = ((player.round_number - 1) % 12) + 1 # Calculate which game (1, 2, or 3) display_game = (player.round_number - 1) // 12 + 1 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 == 36: # or the final round of the 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): # Show disruption notice at the midpoint of each round (i.e., before period 7, 19, and 31) if player.round_number in [6, 18, 30]: return True return False @staticmethod def vars_for_template(player: Player): # Only one treatment is used: HighVarTwoDis disruption_image_path = "my_folder/HighVarDisruption.jpg" return { 'disruption_image_path': disruption_image_path, } @staticmethod def before_next_page(player: Player, timeout_happened): # Set the updated demand standard deviation after disruption # You only have one treatment: HighVarTwoDis, and disruptions at rounds 6, 18, 30 if player.round_number in [6, 18, 30]: player.participant.vars['current_demand_std'] = 200 # Debug print to verify changes print(f"Round {player.round_number}: Std Dev updated 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,36]: return True return False @staticmethod def vars_for_template(player: Player): # Determine which round this is round_number = (player.round_number - 1) // 12 + 1 # Select the correct history for display if round_number == 1: display_game_history = participant.vars.get('round_1_history', []) elif round_number == 2: display_game_history = participant.vars.get('round_2_history', []) else: # round_number == 3 display_game_history = participant.vars.get('round_3_history', []) # Show a reset message at the end of Round 1 or 2 show_reset_message = player.round_number in [12, 24] # Calculate next round number (unless it's already the last round) next_round_number = round_number + 1 if round_number < 3 else None return { 'display_game_history': display_game_history, 'round_number': round_number, 'next_round_number': next_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 [36]: 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]