import datetime import random import time from collections import OrderedDict from datetime import datetime # To handle time properly from django import forms from django.contrib.auth.decorators import login_required from django.http import JsonResponse from otree.api import * from otree.db.models import Model, ForeignKey from . import models # Contains my data structure and functions author = 'Filipp Chapkovskii, UZH, chapkovski@gmail.com' doc = """ Amazon-like auction. Bidding time is extended after each bid. """ class Constants(BaseConstants): instructions_template = 'AuctionAmazon/Instructions.html' name_in_url = 'AuctionAmazon' players_per_group = 2 num_rounds = 5 # starting_time = 30 # extra_time = 20 timeout_seconds = 60 endowment = 100 prize = 100 bid_success_probability = 1.0 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() curstage = models.IntegerField(initial=1) # def time_left(self): # now = time.time() # time_left = self.auctionenddate - now # time_left = round(time_left) if time_left > 0 else 0 # return time_left class Player(BasePlayer): # max_bid = models.IntegerField(initial=0) # bid_submitted_time = models.FloatField() private_value = models.IntegerField() proceed = models.IntegerField(initial=0) preliminary_payoff = models.IntegerField(initial=0) second_stage_bid = models.BooleanField(blank=False) second_stage_bid_amount = models.IntegerField(blank=True) class Bid(Model): 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) player = models.Link(Player) # player = ForeignKey(Player, on_delete=models.CASCADE) # creates 1:m relation -> this bid was submitted by a certain player 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): for bid in Bid.objects.order_by('id'): player = bid.player participant = player.participant yield [ participant.code, player.round_number, 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, Constants.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.objects.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 # for player in self.get_players(): # _ = player.bid_set.create() # _.save() 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_groups = [] for r in range(0, Constants.num_rounds): selected_groups.append(random.choice(subsession.get_group_matrix())) selected_players = [] for round in range(0, Constants.num_rounds): players_and_payoffs = [] for player in selected_groups[round]: # selected_groups[round] is a list of players players_and_payoffs.append(player.in_round(round + 1).participant.label) players_and_payoffs.append(player.in_round(round + 1).preliminary_payoff) selected_players.append(players_and_payoffs) return {'selected_players': selected_players} 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 def live_auction(group: Group, id_in_group, payload): jsonmessage = payload 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) message_for_group = ' ' message_for_player = ' ' stage_info = ' ' if cur_player_bid > Constants.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: print('Bid is legal') Bid.objects.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, ) # new_bid = curbidder.bid_set.create() # new_bid.amount = cur_player_bid # new_bid.at_time = time.time() - self.auctionstartdate # new_bid.in_stage = 1 # new_bid.success = 1 # new_bid.in_round = curbidder.subsession.round_number # new_bid.p_value = curbidder.private_value # new_bid.save() # print(new_bid) curbidder.save() 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: (highest_bid(player).amount, -highest_bid(player).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.' group.save() # 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} # def write_results_to_file(self): # players = '' # for p in self.get_players(): # players += str(p.participant.id_in_session) # players += 'vs' # players = players[:-2] #Cuts off last 'vs' # # with open('Amazon_1.0_started_' # + str(time.strftime('%m_%d_%H_%M_%S',time.localtime(self.auctionstartdate))) # + '_played_by_' # + players # + 'round_' # + str(self.round_number) # + '.txt','w') as f: # # First write down the winner # f.write("The participants payoffs are:" + '\n') # for p in self.get_players(): # f.write("Participant_" + str(p.participant.id_in_session) # + ': ' + str(p.preliminary_payoff) # + '\n'+ '\n'+ '\n') # # Now write down all bids # f.write("The participants bids are:" + '\n') # for p in self.get_players(): # bid_qs = Bid.objects.filter(player__exact=p) # for bid in bid_qs: # f.write(str(bid) + '\n') # f.closed def bids(player: Player): return Bid.objects.filter(player=player) def last_bid(player: Player): sorted_bids = sorted(bids(player), 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(player: Player): sorted_bids = sorted(bids(player), 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] # # def last_bid(self): # sorted_bids = sorted( # self.bid_set.all(), # 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.bid_set.all(), # 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] # PAGES # from pip._vendor.requests.sessions import session # from .utils import get_field_names_for_csv # For custom export 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'] * (100 / group.session.config['point_worth_ct']) + random.random() * ( ( group.session.config['max_private_value'] - group.session.config['min_private_value'] ) * (100 / group.session.config['point_worth_ct']) ) ) class WP(WaitPage): @staticmethod def after_all_players_arrive(group: Group): now = time.time() group.auctionstartdate = now # self.group.auctionenddate = now + Constants.starting_time class ResultsWP(WaitPage): @staticmethod def after_all_players_arrive(group: Group): set_payoffs(group) # self.group.write_results_to_file() 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': Constants.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']) ), } class Stage1(Page): timeout_seconds = Constants.timeout_seconds live_method = live_auction @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 vars_for_template(player: Player): return { 'max_bid': highest_bid(player).amount, 'probability': Constants.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']) ), } # def before_next_page(self): # self.player.second_stage_bid_amount = highest_bid(self.player).amount class Stage2(Page): @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 form_model = models.Player form_fields = ['second_stage_bid', 'second_stage_bid_amount'] @staticmethod def second_stage_bid_amount_max(player: Player): return Constants.endowment @staticmethod def second_stage_bid_amount_min(player: Player): return max(highest_bid(player).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': highest_bid(player).amount, 'probability': Constants.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.second_stage_bid == True: if not player.second_stage_bid_amount == None: # "Illegal" bids are prevented by formfield validation if random.random() <= Constants.bid_success_probability: Bid.objects.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.objects.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: (highest_bid(player).amount, -highest_bid(player).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 == Constants.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'], } # @login_required # def export_view_json(request): # """ # Custom view function to export full results for this game as JSON file # """ # # def create_odict_from_object(obj, fieldnames): # """ # Small helper function to create an OrderedDict from an object using # as attributes. # """ # data = OrderedDict() # for f in fieldnames: # data[f] = getattr(obj, f) # # return data # # # get the complete result data from the database # qs_results = models.Player.objects.select_related('subsession', 'subsession__session', 'group', 'participant')\ # .prefetch_related('bid_set')\ # .all() # # session_fieldnames = [] # will be defined by get_field_names_for_csv # subsess_fieldnames = [] # will be defined by get_field_names_for_csv # group_fieldnames = [] # will be defined by get_field_names_for_csv # player_fieldnames = [] # will be defined by get_field_names_for_csv # bid_fieldnames = ['mode','amount','at_time','in_stage','success','in_round','p_value'] # # # get all sessions, order them by label # sessions = sorted(set([p.subsession.session for p in qs_results]), key=lambda x: x.label) # # # this will be a list that contains data of all sessions # output = [] # # # loop through all sessions # for sess in sessions: # session_fieldnames = session_fieldnames or get_field_names_for_csv(sess.__class__) # sess_output = create_odict_from_object(sess, session_fieldnames) # sess_output['subsessions'] = [] # # # loop through all subsessions (i.e. rounds) ordered by round number # subsessions = sorted(sess.get_subsessions(), key=lambda x: x.round_number) # for subsess in subsessions: # subsess_fieldnames = subsess_fieldnames or get_field_names_for_csv(subsess.__class__) # subsess_output = create_odict_from_object(subsess, subsess_fieldnames) # subsess_output['groups'] = [] # # # loop through all groups ordered by ID # groups = sorted(subsess.get_groups(), key=lambda x: x.id_in_subsession) # for g in groups: # group_fieldnames = group_fieldnames or get_field_names_for_csv(g.__class__) # g_output = create_odict_from_object(g, group_fieldnames) # g_output['players'] = [] # # # loop through all players ordered by ID # players = sorted(g.get_players(), key=lambda x: x.participant.id_in_session) # for p in players: # player_fieldnames = player_fieldnames or get_field_names_for_csv(p.__class__) # p_output = create_odict_from_object(p, player_fieldnames) # # # add some additional player information # p_output['participant_id_in_session'] = p.participant.id_in_session # p_output['bids'] = [] # # # loop through all decisions ordered by ID # bids = p.bid_set.order_by('id') # for bid in bids: # bid_output = create_odict_from_object(bid, bid_fieldnames) # p_output['bids'].append(bid_output) # # g_output['players'].append(p_output) # # subsess_output['groups'].append(g_output) # # sess_output['subsessions'].append(subsess_output) # # output.append(sess_output) # # return JsonResponse(output, safe=False) page_sequence = [ WPstart, InstructionsPage, WP, Stage1, Stage2, InbetweenWP, Stage1, Stage2, InbetweenWP, Stage1, Stage2, InbetweenWP, Stage1, Stage2, InbetweenWP, Stage1, Stage2, InbetweenWP, Stage1, Stage2, InbetweenWP, ResultsWP, Results, # ResultsSummary ]