from otree.api import * import random import time author = 'Maxim Ott, Uni Ulm' doc = """ Ebay-like auction. Bidding time is extended after each bid. """ class C(BaseConstants): INSTRUCTION_TEMPLATE = 'AuctionEbay08/Instructions.html' NAME_IN_URL = 'AuctionEbay08' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 5 # starting_time = 30 # extra_time = 20 TIMEOUT_SECONDS = 60 ENDOWMENT = 100 # in ECU / points PRIZE = 100 # in ECU / points BID_SUCCESS_PROBABILITY = 0.8 NUM_OTHERS = PLAYERS_PER_GROUP - 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): first_price = models.IntegerField(initial=0) second_price = models.IntegerField(initial=0) auctionstartdate = models.FloatField() # auctionenddate = models.FloatField() curwinner = models.IntegerField(initial=-1) curstage = models.IntegerField(initial=1) class Player(BasePlayer): private_value = models.IntegerField() proceed = models.IntegerField(initial=0) preliminary_payoff = models.IntegerField(initial=0) second_stage_bid = models.BooleanField() second_stage_bid_amount = models.IntegerField(blank=True) def bids(self): return Bid.filter(player=self) def last_bid(self): sorted_bids = sorted(self.bids(), key=lambda bid: bid.at_time, reverse=True) sorted_successful_bids = [bid for bid in sorted_bids if bid.success == 1] return sorted_successful_bids[0] def highest_bid(self): sorted_bids = sorted(self.bids(), key=lambda bid: bid.amount, reverse=True) sorted_successful_bids = [bid for bid in sorted_bids if bid.success == 1] return sorted_successful_bids[0] class Bid(ExtraModel): player = models.Link(Player) mode = models.IntegerField(initial=2) amount = models.IntegerField(initial=0) at_time = models.FloatField(initial=0) in_stage = models.IntegerField(initial=0) success = models.IntegerField(initial=1) in_round = models.IntegerField(initial=0) p_value = models.IntegerField(initial=0) def __str__(self): bidstr = "Round " + str(self.player.subsession.round_number) + '\n' bidstr += "Group " + str(self.player.subsession.round_number) + '\n' bidstr += "Player " + str(self.player.id_in_group) + '\n' bidstr += "Value " + str(self.player.private_value) + '\n' bidstr += "Stage " + str(self.in_stage) + '\n' bidstr += "Time " + str(self.at_time) + '\n' bidstr += "Success " + str(self.success) + '\n' bidstr += "Amount " + str(self.amount) + '\n' return bidstr def custom_export(players): print('custom_export() called') # header row yield [ 'session', 'participant_code', 'round_number', 'id_in_group', 'bid_in_stage','bid_at_time','bid_amount' ] for p in players: participant = p.participant session = p.session for bid in p.bids(): yield [ session.code, participant.code, p.round_number, p.id_in_group, bid.in_stage, bid.at_time, bid.amount] # FUNCTIONS def creating_session(subsession: Subsession): # Group randomly with fixed IDs subsession.group_randomly(fixed_id_in_group=True) # There will be a single paying round if subsession.round_number == 1: paying_round = random.randint(1, C.NUM_ROUNDS) subsession.session.vars['paying_round'] = paying_round # Set start time / Not necessary, only for logs for g in subsession.get_groups(): g.first_price = 0 g.second_price = 0 g.auctionstartdate = time.time() for p in subsession.get_players(): Bid.create( player=p, amount=0, at_time=time.time() - g.auctionstartdate, in_stage=0, success=1, in_round=subsession.round_number, p_value=0, ) print('Created bid objects for round ' + str(subsession.round_number)) for p in subsession.get_players(): print(p.bids()) # Add one bid to each player, otherwise error on first screen def vars_for_admin_report(subsession: Subsession): # Randomly select a group for each round # Then look up the players in that group, # and return their IDs, Codenames and Payoffs selected_players_and_payoffs = {} for r in range(0, C.NUM_ROUNDS): selected_group = random.choice(subsession.get_groups()) selected_players_and_payoffs[str(r+1)]=[] for player in selected_group.get_players(): selected_players_and_payoffs[str(r+1)].append(player.in_round(r + 1).participant.label) selected_players_and_payoffs[str(r+1)].append(player.in_round(r + 1).preliminary_payoff) return {'selected_players': selected_players_and_payoffs} def set_payoffs(group: Group): for p in group.get_players(): if str(group.curwinner) == str(p.id_in_group): p.preliminary_payoff = p.private_value - group.second_price else: p.preliminary_payoff = 0 if group.round_number == group.session.vars['paying_round']: p.payoff = p.preliminary_payoff else: p.payoff = 0 class WPstart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): for p in group.get_players(): p.private_value = int( group.session.config['min_private_value'] + random.random()*( group.session.config['max_private_value'] - group.session.config['min_private_value'] ) ) class WP(WaitPage): @staticmethod def after_all_players_arrive(group: Group): now = time.time() group.auctionstartdate = now # self.group.auctionenddate = now + C.starting_time class ResultsWP(WaitPage): @staticmethod def after_all_players_arrive(group: Group): set_payoffs(group) class InbetweenWP(WaitPage): @staticmethod def is_displayed(player: Player): if all(p.second_stage_bid == False for p in player.group.get_players()): return False else: return True @staticmethod def after_all_players_arrive(group: Group): if not all(p.second_stage_bid == False for p in group.get_players()): for pl in group.get_players(): pl.second_stage_bid = True for pl in group.get_players(): pl.second_stage_bid_amount = None class InstructionsPage(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): return { 'probability': C.BID_SUCCESS_PROBABILITY, 'low': int( player.session.config['min_private_value'] ), 'high': int( player.session.config['max_private_value'] ), } class Stage1(Page): timeout_seconds = C.TIMEOUT_SECONDS # To server from page @staticmethod def live_method(player, data): # This is called after liveSend() is called from the html print('Received a message from', player.id_in_group, ':', data) # Disassemble received data jsonmessage = data group = player.group curbidder_id = jsonmessage['id'] curbidder_id_in_group = jsonmessage['id_in_group'] cur_player_bid = int(jsonmessage['cur_player_bid']) curbidder = group.get_player_by_id(curbidder_id_in_group) # Prepare variables, which are to be sent back, to be filled message_for_group = ' ' message_for_player = ' ' stage_info = ' ' # Check if bid is legal if cur_player_bid > C.ENDOWMENT: message_for_player = 'You can not bid more than your maximum ENDOWMENT.' elif cur_player_bid <= curbidder.highest_bid().amount: message_for_player = 'Your new bid must exceed your current maximum bid.' else: # If bid is legal, save it # (If you want to save illegal bids, update the Bid(ExtraModel) to make the distinction) print("Bid is legal. Saved to player's bid list.") Bid.create( player=curbidder, amount=cur_player_bid, at_time=time.time() - group.auctionstartdate, in_stage=1, success=1, in_round=curbidder.subsession.round_number, p_value=curbidder.private_value, ) # Check if bid is relevant, i.e. would change the current price if cur_player_bid <= group.second_price: message_for_player = 'Your new bid must exceed the current selling price.' else: print('Bid is relevant.') # Sort players by their respective maximum bid and then the time they submitted it (in case of duplicates) sorted_players = sorted( group.get_players(), key=lambda player: (player.highest_bid().amount, -player.highest_bid().at_time), reverse=True, ) # Replace the prices group.first_price = sorted_players[0].highest_bid().amount group.second_price = sorted_players[1].highest_bid().amount # Set winner group.curwinner = sorted_players[0].id_in_group # print('winner', mygroup.curwinner) message_for_player = 'Your bid was submitted successfully.' # After the bid/proceed request have been handled, and all variables have # been set, send everything to the group textforgroup = { "price": group.second_price, "winner": group.curwinner, "player_maxbid": curbidder.highest_bid().amount, "message_from_id": curbidder_id_in_group, "message_all": message_for_group, "message_player": message_for_player, } return {0: textforgroup} @staticmethod def vars_for_template(player: Player): return { 'max_bid': player.highest_bid().amount, 'probability': C.BID_SUCCESS_PROBABILITY, 'low': int( player.session.config['min_private_value'] * (player.session.config['point_worth_ct']) ), 'high': int( player.session.config['max_private_value'] * (player.session.config['point_worth_ct']) ), } class Stage2(Page): form_model = 'player' form_fields = ['second_stage_bid', 'second_stage_bid_amount'] @staticmethod def second_stage_bid_amount_max(player: Player): return C.ENDOWMENT @staticmethod def second_stage_bid_amount_min(player: Player): return max(player.highest_bid().amount, player.group.second_price) + 1 @staticmethod def vars_for_template(player: Player): return { 'first_price': player.group.first_price, 'second_price': player.group.second_price, 'winner': player.group.curwinner, 'max_bid': player.highest_bid().amount, 'probability': C.BID_SUCCESS_PROBABILITY, 'low': int( player.session.config['min_private_value'] * (100 / player.session.config['point_worth_ct']) ), 'high': int( player.session.config['max_private_value'] * (100 / player.session.config['point_worth_ct']) ), } @staticmethod def before_next_page(player: Player, timeout_happened): if player.field_maybe_none('second_stage_bid') == True: if not player.field_maybe_none('second_stage_bid_amount') == None: # "Illegal" bids are prevented by formfield validation if random.random() <= C.BID_SUCCESS_PROBABILITY: Bid.create( player=player, amount=player.second_stage_bid_amount, at_time=time.time() - player.group.auctionstartdate, in_stage=2, success=1, in_round=player.subsession.round_number, p_value=player.private_value, ) else: Bid.create( player=player, amount=player.second_stage_bid_amount, at_time=time.time() - player.group.auctionstartdate, in_stage=2, success=0, in_round=player.subsession.round_number, p_value=player.private_value, ) # Ugly hack (conceptually), but it works # Resets only because of rng player.second_stage_bid = False # Sort players by their respective maximum bid and then the time they submitted it (in case of duplicates) sorted_players = sorted( player.group.get_players(), key=lambda player: (player.highest_bid().amount, -player.highest_bid().at_time), reverse=True, ) # Replace the prices player.group.first_price = sorted_players[0].highest_bid().amount player.group.second_price = sorted_players[1].highest_bid().amount # Set winner player.group.curwinner = sorted_players[0].id_in_group # print('winner: ', mygroup.curwinner) else: print('Player submitted last bid empty') class Results(Page): pass class ResultsSummary(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): return { 'total_payoff': sum([p.payoff for p in player.in_all_rounds()]), 'paying_round': player.session.vars['paying_round'], } page_sequence = [ WPstart, InstructionsPage, WP, Stage1, Stage2, ResultsWP, Results, # ResultsSummary ]