from otree.api import * c = cu doc = 'Disruption Game' class C(BaseConstants): NAME_IN_URL = 'DisruptionGame_new' PLAYERS_PER_GROUP = None NUM_ROUNDS = 36 LOW_VAR_DEMAND = (486, 625, 512, 453, 503, 484, 505, 444, 465, 549, 479, 555, 490, 592, 463, 508, 447, 500, 430, 497, 558, 474, 543, 515, 549, 460, 501, 444, 531, 522, 459, 560, 482, 542, 463, 514) LOW_VAR_DIS_DEMAND = (486, 625, 512, 453, 504, 484, 505, 444, 465, 549, 479, 555, 490, 592, 463, 508, 447, 500, 430, 497, 558, 474, 543, 515, 598, 419, 501, 387, 561, 544, 418, 620, 463, 584, 425, 528) RETAIL_COST = 15 WHOLESALE_COST = 6 HOLDING_COST = 1 HIGH_VAR_DEMAND = (472, 750, 524, 407, 508, 468, 511, 389, 430, 598, 457, 610, 479, 684, 426, 517, 393, 500, 359, 493, 616, 448, 585, 530, 598, 419, 501, 387, 561, 544, 418, 620, 463, 584, 425, 528) 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 treatments = ['LowVar', 'LowVarDis', 'HighVar'] weights = [1,0,0] for player in subsession.get_players(): participant = player.participant # Assign treatment and initialize participant vars only once if subsession.round_number == 1: assigned_treatment = choices(treatments, weights=weights, k=1)[0] participant.vars['treatment'] = assigned_treatment # Reset app-specific participant vars so nothing carries over from prior app participant.vars['cumulative_profit'] = 0 participant.vars['game_history'] = [] participant.vars['initial_demand_mean'] = 500 # Assign the correct demand list if assigned_treatment == 'LowVar': participant.vars['demand_list'] = C.LOW_VAR_DEMAND elif assigned_treatment == 'HighVar': participant.vars['demand_list'] = C.HIGH_VAR_DEMAND elif assigned_treatment == 'LowVarDis': participant.vars['demand_list'] = C.LOW_VAR_DIS_DEMAND # Assign payment round once participant.vars['payment_round'] = random.randint(1, C.NUM_ROUNDS) participant.vars['range_payment_round'] = random.choice([6, 18, 27, 34]) # Copy participant-level treatment/payment into player fields each round player.treatment = participant.vars['treatment'] player.payment_round = participant.vars['payment_round'] # Set current variance/image every round so prior app values never persist if player.treatment == 'LowVar': participant.vars['current_demand_std'] = 50 participant.vars['demand_image'] = '50STD.png' elif player.treatment == 'HighVar': participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' elif player.treatment == 'LowVarDis': if subsession.round_number <= 24: participant.vars['current_demand_std'] = 50 participant.vars['demand_image'] = '50STD.png' else: participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' print( f"Round {subsession.round_number}, " f"Participant {participant.id_in_session}, " f"Treatment={player.treatment}, " f"STD={participant.vars['current_demand_std']}, " f"Image={participant.vars['demand_image']}" ) 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() low_range = models.IntegerField(label='What do you think is the lowest realized demand could be in the next round? ', min=0) high_range = models.IntegerField(label='What do you think is the highest realized demand could be in the next round? ', max=1000) end_question = models.StringField(label='How did your ordering strategies change over time? ') range_payment_amount = models.IntegerField() class Range(Page): form_model = 'player' form_fields = ['low_range', 'high_range'] @staticmethod def is_displayed(player: Player): if player.round_number in [6,18,27,34]: return True return False @staticmethod def vars_for_template(player: Player): if player.treatment == "LowVar": distribution_text = "The demand distribution is N~(500,50)." elif player.treatment == "HighVar": distribution_text = "The demand distribution is N~(500,100)." elif player.treatment == "LowVarDis": if player.round_number < 25: distribution_text = "The demand distribution is N~(500,50)." else: distribution_text = "The demand distribution is N~(500,100)." else: distribution_text = "" return { 'distribution_text': distribution_text, } class DisruptionNotice(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): treatment = player.participant.vars.get('treatment') return treatment == 'LowVarDis' and player.round_number == 25 @staticmethod def vars_for_template(player: Player): disruption_image_path = "my_folder/LowVarDisruption.jpg" return { 'disruption_image_path': disruption_image_path, 'new_std': 100, } @staticmethod def before_next_page(player: Player, timeout_happened): treatment = player.participant.vars.get('treatment') if treatment == 'LowVarDis' and player.round_number == 25: player.participant.vars['current_demand_std'] = 100 player.participant.vars['demand_image'] = '100STD.png' class Decision(Page): form_model = 'player' form_fields = ['order_quantity'] @staticmethod def vars_for_template(player: Player): participant = player.participant print( f"Decision page debug -> round={player.round_number}, " f"participant={participant.id_in_session}, " f"treatment={participant.vars.get('treatment')}" ) # Initialize main game variables when this app begins if player.round_number == 1 and 'demand_list' not in participant.vars: treatment = participant.vars.get('treatment') # Reset main game tracking participant.vars['cumulative_profit'] = 0 participant.vars['game_history'] = [] # Assign demand list and initial display info based on treatment if treatment == 'LowVar': participant.vars['demand_list'] = C.LOW_VAR_DEMAND participant.vars['current_demand_std'] = 50 participant.vars['demand_image'] = '50STD.png' elif treatment == 'HighVar': participant.vars['demand_list'] = C.HIGH_VAR_DEMAND participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' elif treatment == 'LowVarDis': participant.vars['demand_list'] = C.LOW_VAR_DIS_DEMAND participant.vars['current_demand_std'] = 50 participant.vars['demand_image'] = '50STD.png' # Set starting inventory if player.round_number == 1: player.starting_inventory = 0 else: player.starting_inventory = player.in_round(player.round_number - 1).ending_inventory # Pull current demand display info demand_image = participant.vars.get('demand_image', '') demand_image_path = f"my_folder/{demand_image}" if demand_image else '' current_demand_std = participant.vars.get('current_demand_std', '') treatment = participant.vars.get('treatment', '') # Get full game history and sort by period full_game_history = sorted( participant.vars.get('game_history', []), key=lambda x: x['period'] ) # Keep only the most recent 15 periods game_history = full_game_history[-15:] # Blank out cumulative profit for current period row if present formatted_history = [] for data in game_history: period_data = data.copy() if period_data['period'] == player.round_number: period_data['cumulative_profit'] = '' formatted_history.append(period_data) return { 'starting_inventory': player.starting_inventory, 'display_round': player.round_number, 'demand_image': demand_image, 'current_demand_std': current_demand_std, 'demand_image_path': demand_image_path, 'treatment': treatment, 'game_history': formatted_history, } @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant # Initialize cumulative profit if needed if 'cumulative_profit' not in participant.vars: participant.vars['cumulative_profit'] = 0 # Get demand for the current round demand_list = participant.vars['demand_list'] current_demand = demand_list[player.round_number - 1] player.du = current_demand # Use 0 if order quantity is blank order_quantity = player.order_quantity or 0 available_inventory = player.starting_inventory + order_quantity # Calculate sales and inventory outcomes units_sold = min(player.du, available_inventory) player.ending_inventory = available_inventory - units_sold player.unmet_demand = player.du - units_sold # Fixed cost parameters retail_cost = 15 wholesale_cost = 6 holding_cost = 1 # Calculate profit player.profit = ( (units_sold * retail_cost) - (order_quantity * wholesale_cost) - (player.ending_inventory * holding_cost) ) # Update cumulative profit participant.vars['cumulative_profit'] += player.profit player.cumulative_profit = participant.vars['cumulative_profit'] # Prepare history row for this period period_data = { 'period': player.round_number, 'starting_inventory': player.starting_inventory, 'order_quantity': player.order_quantity, 'demand_quantity': player.du, 'profit': player.profit, 'cumulative_profit': participant.vars['cumulative_profit'], } # Initialize history if needed if 'game_history' not in participant.vars: participant.vars['game_history'] = [] # Update history if this period already exists, otherwise append 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 placeholder for next period next_period = player.round_number + 1 if next_period <= C.NUM_ROUNDS and not any(d['period'] == next_period for d in participant.vars['game_history']): next_period_data = { 'period': next_period, 'starting_inventory': player.ending_inventory, 'order_quantity': '', 'demand_quantity': '', 'profit': '', 'cumulative_profit': participant.vars['cumulative_profit'], } participant.vars['game_history'].append(next_period_data) # Update display image/std after disruption for LowVarDis treatment = participant.vars.get('treatment') if treatment == 'LowVarDis' and player.round_number >= 24: participant.vars['current_demand_std'] = 100 participant.vars['demand_image'] = '100STD.png' class Results(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): return { 'display_round': player.round_number, 'demand': player.du, 'profit': player.profit, 'cumulative_profit': player.cumulative_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): participant = player.participant # Optional debug print print( f"Period {player.round_number}: " f"Starting inventory={player.starting_inventory}, " f"Order quantity={player.order_quantity}, " f"Demand={player.du}, " f"Ending inventory={player.ending_inventory}, " f"Profit={player.profit}" ) # Final payment calculation only at the end of the game if player.round_number == C.NUM_ROUNDS: payment_round = participant.vars['payment_round'] payment_round_profit = player.in_round(payment_round).profit payment_amount = payment_round_profit * 0.001 participant.vars['payment_amount'] = payment_amount player.payment_amount = payment_amount player.payment_round = payment_round print(f"End of Round {C.NUM_ROUNDS}, game_history: {participant.vars.get('game_history', [])}") print(f"Payment round: {payment_round}, Payment amount: {payment_amount}") class Final_question(Page): form_model = 'player' form_fields = ['end_question'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def before_next_page(player: Player, timeout_happened): from datetime import datetime participant = player.participant payment_amount = participant.vars.get('payment_amount', 0) payment_round = participant.vars.get('payment_round') range_payment_round = participant.vars.get('range_payment_round') selected_range_player = player.in_round(range_payment_round) realized_demand = selected_range_player.du lower = selected_range_player.low_range upper = selected_range_player.high_range if lower is not None and upper is not None and lower <= realized_demand <= upper: range_bonus = 1.50 - 0.0015 * (upper - lower) range_bonus = max(range_bonus, 0) else: range_bonus = 0 total_payoff = payment_amount + 2.50 + range_bonus participant.vars['payment_amount'] = payment_amount participant.vars['payment_round'] = payment_round participant.vars['range_payment_round'] = range_payment_round participant.vars['realized_demand'] = realized_demand participant.vars['lower'] = lower participant.vars['upper'] = upper participant.vars['range_bonus'] = range_bonus participant.vars['total_payoff'] = total_payoff player.payment_amount = payment_amount player.total_payoff = total_payoff 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): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): participant = player.participant payment_amount = participant.vars.get('payment_amount', 0) payment_round = participant.vars.get('payment_round') range_payment_round = participant.vars.get('range_payment_round') realized_demand = participant.vars.get('realized_demand') lower = participant.vars.get('lower') upper = participant.vars.get('upper') range_bonus = participant.vars.get('range_bonus', 0) total_payoff = participant.vars.get('total_payoff', 0) return { 'payment_amount': payment_amount, 'payment_round': payment_round, 'range_payment_round': range_payment_round, 'realized_demand': realized_demand, 'lower': lower, 'upper': upper, 'range_bonus': range_bonus, 'total_payoff': total_payoff, } @staticmethod def before_next_page(player: Player, timeout_happened): from datetime import datetime participant = player.participant payment_amount = participant.vars.get('payment_amount', 0) range_bonus = participant.vars.get('range_bonus', 0) player.payment_amount = payment_amount player.total_payoff = payment_amount + 1.50 + range_bonus player.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") page_sequence = [Range, DisruptionNotice, Decision, Results, Final_question, Final_Results]