from otree.api import * c = cu doc = 'BDM auction to elicit WTP/ pain of paying of the spender. One person (spender) participates in auction while other (waiter) waits until next phase. If spender is successful in auction, fills out information for either lab pick-up or shipping.' class C(BaseConstants): # built-in constants NAME_IN_URL = 'Auction_2' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 # user-defined constants NUM_POP_QS = 40 PRICE_STEPS = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) SMILEY_LIKERT_OPTS = (1, 2, 3, 4, 5, 6, 7) AUCTION_INSTRUCTIONS_TEMPLATE = 'Auction_2/auction_instructions.html' UW_TETON_BG_TEMPLATE = 'Auction_2/uw_teton_bg.html' UW_TETON_SURVEY_TEMPLATE = 'Auction_2/uw_teton_survey.html' TWO_COLUMN_FLEX_CONTAINER_TEMPLATE = 'Auction_2/two_column_flex_container.html' class Subsession(BaseSubsession): pass def after_all_players_arrive(subsession: Subsession): session = subsession.session # pull the matrix you stored in the code‐entry app matrix = subsession.session.vars.get('couple_matrix') if not matrix: # safety check in case someone forgot to run grouping first raise RuntimeError("No couple_matrix in session.vars") # re‐apply it in this app's subsession subsession.set_group_matrix(matrix) class Group(BaseGroup): budget = models.IntegerField() price = models.IntegerField() win = models.BooleanField() WTP_spender = models.FloatField() budget_initial = models.IntegerField() def set_price(group: Group): # import random import random # Gather players players = group.get_players() # Define the group budget from RET1 group.budget = players[0].participant.budget_2 group.budget_initial = players[0].participant.budget_2 # Draw a single random number to use as the price group.price = int((random.uniform((group.budget*0.85), group.budget)) + 0.5) # UNIFORM DIST VERSION # Sanity check for price bounds and bounding the distribution if normal if group.price <= 1: group.price = 1 elif group.price >= group.budget: group.price = group.budget def auction_check(group: Group): # Gather players players = group.get_players() # Identify the player with the spending role spender_player = None for p in players: p.participant.WTP_2 = 0 # set default value of WTP to participant field if not used if p.participant.spender == 1: spender_player = p p.participant.WTP_2 = spender_player.WTP # set WTP for spender to participant field # Compare the WTP to price if spender_player.WTP >= group.price: group.win = True group.budget = group.budget - group.price # Money taken from joint account else: group.win = False group.budget = group.budget # Nothing spent from joint account # If winner, save to participant field for p in players: if p.participant.spender == 1 and group.win == True: p.participant.winner_2 = 1 # Save final earnings to a global variable for p in players: p.participant.earnings_2 = group.budget /2 p.participant.computer_price_2 = group.price class Player(BasePlayer): won_auction = models.BooleanField(initial=False) WTP = models.FloatField(label='How much would you be willing to spend on the item shown above in dollars? (Enter your bid as a dollar value using at maximum 2 decimals)', min=0) def WTP_max(player: Player): group = player.group return group.budget def custom_export(players): # Header yield [ 'session_code', 'participant_id_in_session', 'participant_code', 'id_in_group', 'group_member', 'spender', 'earner_round_2', 'WTP_2', 'computer_price_2', 'won_auction_2' ] # Rows for p in players: pp = p.participant pg = p.group ps = p.session # Pull participant fields spender = p.participant.vars.get('spender', '') earner_round_2 = p.participant.vars.get('earner_round_2', '') computer_price_2 = p.participant.vars.get('computer_price_2', '') yield [ ps.code, pp.id_in_session, pp.code, p.id_in_group, pg.id_in_subsession, spender, earner_round_2, p.WTP, computer_price_2, pg.win ] class GroupingWaitPage(WaitPage): wait_for_all_groups = True after_all_players_arrive = after_all_players_arrive class Auction_2StartPage(WaitPage): wait_for_all_groups = True class SetBudgetWaitPage(WaitPage): after_all_players_arrive = set_price class RoleReveal(Page): form_model = 'player' class NotSpendingWaitPage(WaitPage): title_text = 'You were not selected for the spending task. Please wait for further instructions.' @staticmethod def is_displayed(player: Player): participant = player.participant # If the participant is not the spender, sit on this wait page if participant.spender != 1: return True class Spending(Page): timeout_seconds = 180 form_model = 'player' form_fields = ['WTP'] @staticmethod def is_displayed(player: Player): participant = player.participant # If participant is the spender, show them this page return participant.spender == 1 # vars_for_template is deprecated in favor of python-based renderers @staticmethod def vars_for_template(player: Player): participant = player.participant # Define the list of items with unique identifiers, their associated image URLs, and a description imgur_urls = [ {'id': 'A', 'url': 'https://i.imgur.com/OfsrPbR.jpeg', 'description': "Coloring book with pencils"}, {'id': 'B', 'url': 'https://i.imgur.com/tmBYFwX.jpeg', 'description': "Headphones"}, {'id': 'C', 'url': 'https://i.imgur.com/oBfC4Mz.jpeg', 'description': "Moisturizer"}, {'id': 'D', 'url': 'https://i.imgur.com/q7bBuv6.jpeg', 'description': "Spa kit"}, {'id': 'E', 'url': 'https://i.imgur.com/Zc7vR6m.jpeg', 'description': "Grilling accessories"}, {'id': 'F', 'url': 'https://i.imgur.com/YLEoKGI.jpeg', 'description': "Tactical flashlight"}, {'id': 'G', 'url': 'https://i.imgur.com/lmMFuFL.jpeg', 'description': "Lip Balm"}, {'id': 'H', 'url': 'https://i.imgur.com/5KfUIfj.jpeg', 'description': "Water bottle"}, {'id': 'I', 'url': 'https://i.imgur.com/AS1feBP.jpeg', 'description': "Car detailing kit"}, {'id': 'J', 'url': 'https://i.imgur.com/SfJ7GmF.jpeg', 'description': "Multi-tool"} ] # Default fallback if selected_item is missing or invalid default_image = 'https://miro.medium.com/v2/resize:fit:1100/format:webp/1*MXyMqcEJ6Se0SCWcYCKZTQ.jpeg' default_desc = 'No description available.' # Retrieve the selected item from participant data selected_item = participant.selected_item # Expect one of 'A' through 'J' # Find the dictionary for the selected item selected = next((item for item in imgur_urls if item['id'] == selected_item), {'url': default_image, 'description': default_desc}) # Pass both the full list and the selected item's data to the template return dict( image_paths=imgur_urls, image_path=selected['url'], description=selected['description'], ) @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group participant = player.participant group.WTP_spender = player.WTP if timeout_happened: # Default value for any form fields if timeout occurs, because otherwise they may be left null player.player_sum = 0 participant.WTP_2_timeout = 1 class ResultsWaitPage(WaitPage): after_all_players_arrive = auction_check class Auction_1EndPage(WaitPage): wait_for_all_groups = True title_text = 'Please wait for further instructions.' page_sequence = [GroupingWaitPage, Auction_2StartPage, SetBudgetWaitPage, RoleReveal, NotSpendingWaitPage, Spending, ResultsWaitPage, Auction_1EndPage]