# Sky Vercauteren # WageNegotiations # July 2020 from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) from django.conf import settings import random import math author = "Sky Vercauteren" doc = """The setup includes "logging in", selecting a gender identity, completing 2 rounds of practice, and then completing the first initial counting placement task. """ class Constants(BaseConstants): name_in_url = 'WageNegotiations' players_per_group = 2 num_rounds = 2 num_init_rounds = 2 num_main_rounds = 11 """ session.vars = [initial_digits, num_rounds] participant.vars = [Role, name, email, gender, score, standing, prev_earnings, initial_score] """ class Subsession(BaseSubsession): num_init_matrix = models.IntegerField(initial=0) matrix_size = models.IntegerField(initial=0) num_main_matrix = models.IntegerField(initial=0) # set Fallback treatment fallback = models.BooleanField() # generate matrix contents for this round here.. def creating_session(self): # initial self.fallback = self.session.config['FALLBACK'] self.group_randomly(fixed_id_in_group=True) self.num_main_matrix = self.session.config['NUM_MATRICES'] self.num_init_matrix = self.session.config['INITIAL_MATRICES'] self.matrix_size = self.session.config['MATRIX_SIZE'] if self.round_number == 1: # populate initial matrices initlist = [] initmtx = [] initrow = [] for m in range(0, self.num_init_matrix * Constants.num_init_rounds + 1): for r in range(0, self.matrix_size): for X in range(0, self.matrix_size): digit = random.randint(0, 1) initrow.append(digit) initmtx.append(initrow) initrow = [] initlist.append(initmtx) initmtx = [] self.session.vars['initial_digits'] = initlist mainlist = [] mtx = [] row = [] for m in range(0, self.num_main_matrix * Constants.num_main_rounds * 2 + 1): for r in range(0, self.matrix_size): for x in range(0, self.matrix_size): digit = random.randint(0, 1) row.append(digit) mtx.append(row) row = [] mainlist.append(mtx) mtx = [] self.session.vars['main_digits'] = mainlist def set_scores(self): # calculate average score and each players standing total = 0 for p in self.get_players(): total = total + p.score standing = 1 for op in self.get_players(): if op.score > p.score: standing += 1 p.progress = 0 p.participant.vars['standing'] = standing p.participant.vars['score'] = p.score p.participant.vars['prev_earnings'] = 0 p.participant.vars['sum_negotiations'] = 0 p.participant.vars['sum_work'] = 0 p.participant.vars['practice_earnings'] = p.payoff # Store avg score. for g in self.get_groups(): g.avgScore = round(total / self.session.num_participants, 2) def set_practice_scores(self): # calculate average score for g in self.get_groups(): total = 0 for p in g.get_players(): total = total + p.score for pl in g.get_players(): if g.employeeAgreement == 0: earnings = 0 elif pl.role() == 'Employee': earnings = int(total * (g.employeeAgreement / 100)) else: earnings = int(total * ((100 - g.employeeAgreement) / 100)) pl.participant.vars['practice_earnings'] = earnings + pl.participant.vars['practice_earnings'] pl.participant.vars['score'] = pl.score pl.participant.vars['prev_earnings'] = earnings pass class Group(BaseGroup): # Work Variables avgScore = models.FloatField() # Negotiation Variables employeeOffer = models.IntegerField(initial=0) employerOffer = models.IntegerField(initial=0) employeeAgreement = models.IntegerField(initial=0) agreed = models.BooleanField(initial=False) # This method defines the live behavior for the initial work page def live_initial_work(self, id_in_group, attempt): # First, check the users progress, if the page is new, it will be -999 and will set the initial matrix prog = self.get_player_by_id(id_in_group).progress if attempt == -999: self.get_player_by_id(id_in_group).progress = 0 return {id_in_group: {'correct': 0, 'percent': 0, 'progress': self.get_player_by_id(id_in_group).progress, 'matrix': self.session.vars['initial_digits'][0], 'initial': self.session.config['INITIAL_MATRICES'], 'size': self.subsession.matrix_size, 'finished': False}} # Otherwise, if they are somewhere in the middle, calculate correct answer, store it, and advance them if self.subsession.num_init_matrix >= prog >= 0: answer = 0 for row in self.session.vars['initial_digits'][prog]: for col in row: if col == 1: answer += 1 if attempt == answer: self.get_player_by_id(id_in_group).correct_init_answers += 1 cor = self.get_player_by_id(id_in_group).correct_init_answers perc = 0 if cor > 0: perc = cor / self.subsession.num_init_matrix * 100 prog += 1 self.get_player_by_id(id_in_group).progress = prog self.get_player_by_id(id_in_group).score = cor # if the user has finished all the matrices, submit the page. if prog < self.subsession.num_init_matrix: return {id_in_group: {'correct': cor, 'progress': self.get_player_by_id(id_in_group).progress, 'percent': perc, 'matrix': self.session.vars['initial_digits'][prog], 'initial': self.session.config['INITIAL_MATRICES'], 'size': self.subsession.matrix_size, 'finished': False}} else: return {id_in_group: {'correct': cor, 'percent': perc, 'progress': self.get_player_by_id(id_in_group).progress, 'matrix': self.session.vars['initial_digits'][ self.subsession.num_init_matrix - 1], 'initial': self.session.config['INITIAL_MATRICES'], 'size': self.subsession.matrix_size, 'finished': True}} def live_negotiate(self, id_in_group, offer): if self.agreed: return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} # This is the "refresh" on the live page. if offer == -100: # first, set the negotiated percent every time, in case of a timeout. if self.employeeAgreement == 0: self.get_player_by_id(id_in_group).participant.vars['negotiation'] = 0 else: if self.get_player_by_id(id_in_group).role() == 'Employee': self.get_player_by_id(id_in_group).participant.vars['negotiation'] = self.employeeAgreement / 100 else: self.get_player_by_id(id_in_group).participant.vars['negotiation'] = ( 100 - self.employeeAgreement) / 100 return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} # First, check everything against the role. role = self.get_player_by_id(id_in_group).role() if role == 'Employee': # Then we check if the user accepted if offer == -999 or int(offer) == self.employerOffer: if self.employerOffer > 0: self.employeeAgreement = self.employerOffer self.agreed = True # store negotiated amounts self.get_player_by_id(id_in_group).accepted = True self.get_player_by_id(id_in_group).participant.vars['negotiation'] = self.employeeAgreement / 100 for p in self.get_players(): if p.id_in_group != id_in_group: p.participant.vars['negotiation'] = (100 - self.employeeAgreement) / 100 return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} # If we made it this far we must be making an offer tempOffer = self.employeeOffer if self.employeeOffer == 0: tempOffer = 100 if int(offer) < tempOffer: self.get_player_by_id(id_in_group).current_offer = int(offer) self.employeeOffer = int(offer) return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} else: # so we check if the user accepted if offer == -999 or int(offer) == self.employeeOffer and self.employeeOffer > 0: if self.employeeOffer > 0: self.agreed = True self.employeeAgreement = self.employeeOffer # store negotiated amounts self.get_player_by_id(id_in_group).accepted = True self.get_player_by_id(id_in_group).participant.vars['negotiation'] = ( 100 - self.employeeAgreement) / 100 for p in self.get_players(): if p.id_in_group != id_in_group: p.participant.vars['negotiation'] = self.employeeAgreement / 100 return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} # So we must be making an offer if int(offer) > self.employerOffer: self.get_player_by_id(id_in_group).current_offer = int(offer) self.employerOffer = int(offer) return {id_in_group: {'EE_offer': self.employeeOffer, 'ER_offer': self.employerOffer, 'time': self.session.config['NEGOTIATION_TIME'] + 15, 'role': self.get_player_by_id(id_in_group).role(), 'finished': self.agreed}} # This is a customized wait page designed to catch errors caused by a lost server connection or lag. def live_partner_wait(self, id_in_group, info): # set self and partner user = self.get_player_by_id(id_in_group) partner = None for p in self.get_players(): if p.id_in_group != id_in_group: partner = p # set waiting to true user.partnerWaiting = True # Throw broken data if user has been waiting too long. if info == -99: user.brokenData = True partner.brokenData = True # then see if the data matches. if user.participant.vars['negotiation'] > 0 or user.accepted: # If one player has an agreement, then both should. set agreement data. otherwise they did not agree. if self.agreed is False or self.employeeAgreement == 0: print("should only get here if it really broke!") user.brokenData = True partner.brokenData = True # Fix it! self.agreed = True if self.get_player_by_id(id_in_group).role == 'Employee': self.employeeAgreement = user.participant.vars['negotiation'] * 100 else: self.employeeAgreement = (1 - user.participant.vars['negotiation']) * 100 # and lastly reset the correct amount for the disconnected user. partner.participant.vars['negotiation'] = (1 - user.participant.vars['negotiation']) # once we've done that we just keep waiting for the partner to reconnect. if partner.partnerWaiting: # lets do a final check now that both users are on this page. # if either user "Accepts" then the page should not timeout. if (user.accepted and partner.expired) or (partner.accepted and user.expired): user.brokenData = True partner.brokenData = True # then we can submit the page or keep waiting. return {id_in_group: {'finished': 100}} else: return {id_in_group: {'finished': -100}} # This method defines the live behavior for the initial work page def live_work(self, id_in_group, attempt): neg = self.get_player_by_id(id_in_group).participant.vars['negotiation'] prog = self.get_player_by_id(id_in_group).progress + ((self.round_number - 1) * self.subsession.num_main_matrix) # This is the refresh method. It will update the "earnings" if attempt == -100: cor = self.get_player_by_id(id_in_group).correct_answers perc = 0 if cor > 0: perc = (cor / self.subsession.num_main_matrix) * 100 totalScore = cor * self.session.config['POINTS_PER_ANSWER'] for p in self.get_players(): if p.id_in_group != id_in_group: opID = p.participant.id_in_session self.get_player_by_id(id_in_group).opponent_session_ID = opID totalScore = totalScore + p.score earn = int(totalScore * neg) return {id_in_group: {'correct': cor, 'earn': earn, 'progress': self.get_player_by_id(id_in_group).progress, 'percent': perc, 'matrix': self.session.vars['main_digits'][prog], 'time': self.session.config['MAIN_WORK_TIME'], 'main': self.session.config['NUM_MATRICES'], 'size': self.subsession.matrix_size, 'finished': False}} # Otherwise, if they are somewhere in the middle, calculate correct answer, store it, and advance them if self.subsession.num_main_matrix * self.round_number >= prog >= ( self.round_number - 1) * self.subsession.num_main_matrix: answer = 0 for row in self.session.vars['main_digits'][prog]: for col in row: if col == 1: answer += 1 if attempt == answer: self.get_player_by_id(id_in_group).correct_answers += 1 # Calculate the number correct, score, and earnings. cor = self.get_player_by_id(id_in_group).correct_answers score = cor * self.session.config['POINTS_PER_ANSWER'] totalScore = score for p in self.get_players(): if p.id_in_group != id_in_group: totalScore = totalScore + p.score earn = int(totalScore * neg) perc = 0 if cor > 0: perc = int((cor / self.subsession.num_main_matrix) * 100) prog += 1 self.get_player_by_id(id_in_group).progress = prog - ( (self.round_number - 1) * self.subsession.num_main_matrix) self.get_player_by_id(id_in_group).score = score # if the user has finished all the matrices, submit the page. if prog < self.round_number * self.subsession.num_main_matrix: return { id_in_group: {'correct': cor, 'earn': earn, 'progress': self.get_player_by_id(id_in_group).progress, 'percent': perc, 'matrix': self.session.vars['main_digits'][prog], 'time': self.session.config['MAIN_WORK_TIME'], 'main': self.session.config['NUM_MATRICES'], 'size': self.subsession.matrix_size, 'finished': False}} else: return {id_in_group: {'correct': cor, 'earn': earn, 'percent': perc, 'progress': self.get_player_by_id(id_in_group).progress, 'matrix': self.session.vars['main_digits'][self.subsession.num_main_matrix - 1], 'time': self.session.config['MAIN_WORK_TIME'], 'main': self.session.config['NUM_MATRICES'], 'size': self.subsession.matrix_size, 'finished': True}} def special_wait(self): for p in self.get_players(): p.participant.vars['progress'] = self.round_number * self.subsession.num_init_matrix p.participant.vars['negotiation'] = 0 p.participant.vars['score'] = 0 p.participant.vars['prev_earnings'] = 0 p.participant.vars['standing'] = 0 p.participant.vars['initial_score'] = 0 pass class Player(BasePlayer): # Initial Variables agree = models.StringField(choices=["Yes", "No"], widget=widgets.RadioSelect, label="") name = models.StringField() email = models.StringField() gender = models.StringField(choices=["Male/Man", "Female/Woman"]) # negotiation variables accepted = models.BooleanField(initial=False) expired = models.BooleanField(initial=False) partnerWaiting = models.BooleanField(initial=False) brokenData = models.BooleanField(initial=False) def agree_error_message(self, value): if value == 'No': return 'Please make sure that you have read and understood the above information, then select \"Yes\" to continue.' def email_error_message(self, value): at = False dot = False i = 0 for x in value: if x == '@': at = True if x == '.': dot = True i = i + 1 if at is False or dot is False or i < 6: return 'Hmm, I don\'t seem to recognize that email address. Please enter a valid address.' def role(self): return self.participant.vars['Role'] # Work Variables progress = models.IntegerField(initial=0) correct_init_answers = models.IntegerField(initial=0) correct_answers = models.IntegerField(initial=0) def num_matrix(self): return self.subsession.num_main_matrix def num_init_matrix(self): return self.subsession.num_init_matrix score = models.IntegerField(initial=0) # Practice Variables current_offer = models.IntegerField(initial=0) opponent_session_ID = models.IntegerField() pass