from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range ) from django.db import models as djmodels import random import itertools from django.db.models.signals import post_save from django.db.models import Sum author = 'Ryan Oprea, roprea@gmail.com' doc = """ Walrasian Equilibrium Selection The only real tricky thing in the software is the implementation of the game's timing which hs two features that aren't simple for oTree: 1. Groups need to run at their own pace for irregular numbers of rounds in each market but when they are all finished or they run out of rounds they need to be regrouped with one another. 2. Groups can finish trading long before the maximum number of rounds completes. In order to deal with this, the software does the following: 1. When a group finishes, it is shown the Results page and the current round is stored in a session variable. 2. When both groups finish, a channel message is sent to all players. This message enables the Next button. 3. Participant next_round variables track which is the next round they should be active in. When both groups finish, these variables are set to the maximum round in which one of the two groups finished (+1). Thus, the group that finished in an earlier round will skip ahead to catch up to the other group. 4. New groups are formed and a new market begins. """ class Constants(BaseConstants): name_in_url = 'walras' # Size of market players_per_group = 4 # Total number of rounds num_rounds = 250 # Total number of markets (in config) num_markets = 51 num_to_pay = 3 # number of markets to pay dollars_per_point = 0.333 # dollars for one experimental point participation_fee = 5 class Subsession(BaseSubsession): ### Get configuration data from file def readconfig(self): data = {} import csv with open(r'./_static/global/configwalras.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') header = next(reader) for name in header: data[name] = [] # read rows, append values to lists for row in reader: for i, value in enumerate(row): data[header[i]].append(value) return data #### Re-shuffle groups; minround and maxround are rounds to set the group in def form_groups(self,minround,maxround): buyers_per_group = Constants.players_per_group // 2 for subsession in self.in_rounds(minround, maxround): players = subsession.get_players() B_players = [p for p in players if p.participant.vars['type'] == 'buyer'] S_players = [p for p in players if p.participant.vars['type'] == 'seller'] group_matrix = [] for j in range(len(self.session.vars['buyers'][self.session.vars['current_market']-1])//2): new_group = [] for i in range(buyers_per_group): new_group.append(B_players[self.session.vars['buyers'][self.session.vars['current_market']-1][j*buyers_per_group+i]-1]) new_group.append(S_players[self.session.vars['sellers'][self.session.vars['current_market']-1][j*buyers_per_group+i]-1]) group_matrix.append(new_group) subsession.set_group_matrix(group_matrix) #### At the beginning of each each round def creating_session(self): if self.round_number==1: types = itertools.cycle(['buyer', 'seller']) for p in self.get_players(): p.participant.vars['type'] = next(types) p.participant.vars['market_earn'] = 0 p.participant.vars['market_earn_list'] = [] p.participant.vars['market_payments'] = [0]*Constants.num_to_pay p.participant.vars['saw_results'] = 0 p.participant.vars['first_trade_period']= 0 # per group but need to store by player # read config and parse on first round only self.session.vars['configdata'] = self.readconfig() configdata = self.session.vars['configdata'] # local variable for convenience # initialization self.session.vars['value_cost'] = [] self.session.vars['init_round'] = 1 # initial round for market self.session.vars['buyers'] = [] # indicates which buyers are grouped self.session.vars['sellers'] = [] # indicates which buyers are grouped self.session.vars['match'] = [] self.session.vars['proposers'] = [] self.session.vars['paid_markets'] = [] # which markets were randomly chosen for payment # config from first market only (fixed across markets) self.session.vars['anonymous'] = configdata["anonymous"][0] self.session.vars['time_series'] = configdata["time_series"][0] self.session.vars['multiresponder'] = int(configdata["multiresponder"][0]) value_cost = configdata["value_cost"][0] value_cost_int = [int(x) for x in value_cost.split(',')] for x in value_cost_int: self.session.vars['value_cost'].append(x) # config variables that change by market for index in range(0, Constants.num_markets): buyers = configdata["buyers"][index] buyers_int = [int(x) for x in buyers.split(',')] self.session.vars['buyers'].append(buyers_int) sellers = configdata["sellers"][index] sellers_int = [int(x) for x in sellers.split(',')] self.session.vars['sellers'].append(sellers_int) match = configdata["match"][index] match_int = [int(x) for x in match.split(',')] # replicate matches and proposers some large number of times matchlist = [] for m in range(1, 20): matchlist.extend(match_int) self.session.vars['match'].append(matchlist) proposers = configdata["proposers"][index] proposers_int = [int(x) for x in proposers.split(',')] proposerlist = [] for m in range(1, 20): proposerlist.extend(proposers_int) self.session.vars['proposers'].append(proposerlist) self.session.vars['current_market'] = 1 # current market for p in self.get_players(): p.participant.vars['traded']=False p.participant.vars['trading_price'] = -1 # set initial group self.form_groups(1,1) #### Called at the end of each market. Adds each player's payoff to a list and then randomly chooses 3 markets to # pay. The results are displayed on the Admin Report page and are updated every time a market ends (with potentially # different markets being paid). def randomize_payments(self): for p in self.get_players(): p.participant.vars['market_earn_list'].append(p.participant.vars['market_earn']) num_complete_markets = len(p.participant.vars['market_earn_list']) markets_to_pay = random.sample(range(num_complete_markets),min(Constants.num_to_pay,num_complete_markets)) self.session.vars['paid_markets'] = markets_to_pay for p in self.get_players(): for i in range(min(Constants.num_to_pay,num_complete_markets)): p.participant.vars['market_payments'][i] = p.participant.vars['market_earn_list'][markets_to_pay[i]] p.participant.vars['total_payment'] = c(sum(p.participant.vars['market_payments'])).to_real_world_currency(self.session) + self.session.config['participation_fee'] def vars_for_admin_report(self): return {'paid_markets': self.session.vars['paid_markets'], 'participants': self.get_players()} def both_groups_done(self): numdone = 0 for g in self.get_groups(): if g.market_ends(): numdone = numdone + 1 return numdone == 2 # period, relative to start of market def period(self): return self.round_number-self.session.vars['init_round'] + 1 class Group(BaseGroup): # How much has been offered? amount_offered = models.IntegerField(min=-10, max=110,label="Make Proposal") # What is the id_in_group of the active buyer? active_buyer = models.IntegerField() active_buyer2 = models.IntegerField() # in case of multiresponder # What is the id_in_group of the active seller? active_seller = models.IntegerField() active_seller2 = models.IntegerField() # in case of multiresponder # What is the id_in_group of the proposer proposer = models.IntegerField() # Did a proposer pass? #proposer_pass = models.BooleanField(initial=False) # Indicator that market ended(for recording purposes only) market_ended=models.IntegerField() # did someone in the group accept the offer someone_accepted = models.BooleanField() #### Returns identity of first responder def first_responder(self): if self.proposer == self.active_buyer: return self.active_seller else: return self.active_buyer #### Returns string ID of first responder def first_responder_id(self): if self.proposer == self.active_buyer: return 'S' + str(self.session.vars['value_cost'][self.active_seller - 1]) else: return 'B' + str(self.session.vars['value_cost'][self.active_buyer - 1]) #### Did first responder accept? def first_accepted(self): return [p.offer_accepted for p in self.get_players() if p.id_in_group==self.first_responder()][0] #### Did second responder accept? def second_accepted(self): return [p.offer_accepted for p in self.get_players() if p.id_in_group==self.second_responder()][0] #### Returns identity of second responder def second_responder(self): if self.proposer == self.active_buyer: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'seller'] if responder_list[0] == self.active_seller: return responder_list[1] else: return responder_list[0] else: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'buyer'] if responder_list[0] == self.active_buyer: return responder_list[1] else: return responder_list[0] #### Returns string ID of second responder def second_responder_id(self): if self.proposer == self.active_buyer: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'seller'] if responder_list[0] == self.active_seller: return 'S' + str(self.session.vars['value_cost'][responder_list[1] - 1]) else: return 'S' + str(self.session.vars['value_cost'][responder_list[0] - 1]) else: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'buyer'] if responder_list[0] == self.active_buyer: return 'B' + str(self.session.vars['value_cost'][responder_list[1] - 1]) else: return 'B' + str(self.session.vars['value_cost'][responder_list[0] - 1]) #### Returns identify of subject that accepted the trade def acceptor_id(self): first_accept = [p.offer_accepted for p in self.get_players() if p.id_in_group == self.first_responder()][0] # should be exactly one match if self.proposer == self.active_buyer: if first_accept: return 'S' + str(self.session.vars['value_cost'][self.active_seller - 1]) else: return 'S' + str(self.session.vars['value_cost'][self.active_seller2 - 1]) else: if first_accept: return 'B' + str(self.session.vars['value_cost'][self.active_buyer - 1]) else: return 'B' + str(self.session.vars['value_cost'][self.active_buyer2 - 1]) # remove future matches (including current period) of subjects that have already traded def exclude_matches(self): excludematch = [0, 0, 0, 0] # match combinations to be excluded # identify matches involving players that have traded for p in self.get_players(): if p.participant.vars['traded'] == 1: if p.id_in_group == 1: excludematch[0] = 1 excludematch[1] = 1 elif p.id_in_group == 2: excludematch[1] = 1 excludematch[3] = 1 elif p.id_in_group == 3: excludematch[2] = 1 excludematch[3] = 1 elif p.id_in_group == 4: excludematch[0] = 1 excludematch[2] = 1 first_trade_period = p.participant.vars['first_trade_period'] # picks up first trade period from any player (all same) matchlist = self.session.vars['match'][self.session.vars['current_market'] - 1] proposerlist = self.session.vars['proposers'][self.session.vars['current_market'] - 1] if first_trade_period == 0: # no trade occurred matches = matchlist proposers = proposerlist else: matches = [] proposers = [] for m in range(0, first_trade_period): matches.append(matchlist[m]) proposers.append(proposerlist[m]) for m in range(first_trade_period, len(matchlist)): if excludematch[matchlist[m]-1] == 0: matches.append(matchlist[m]) proposers.append(proposerlist[m]) # return matches for this and future periods return matches[self.subsession.period()-1:], proposers[self.subsession.period()-1:] #### Assign active players and select a proposer at random def make_match(self): validmatches, validproposers = self.exclude_matches() if len(validmatches)>0 and len(validproposers)>0: current_match = validmatches[0] if current_match == 1: # high value buyer and low cost seller seller = 4 buyer = 1 elif current_match == 2: # high value buyer and high cost seller seller = 2 buyer = 1 elif current_match == 3: # low value buyer and low cost seller seller = 4 buyer = 3 else: # low value buyer and high cost seller seller = 2 buyer = 3 if validproposers[0] == 1: self.proposer = buyer else: self.proposer = seller elif self.market_exhausted==False: # get here if length of preset matches is too short (shouldn't happen) seller = random.choice([p.id_in_group for p in self.get_players() if (p.participant.vars['type']=='seller' and p.traded==False)]) buyer = random.choice([p.id_in_group for p in self.get_players() if (p.participant.vars['type']=='buyer' and p.traded==False)]) self.proposer=random.choice([buyer,seller]) else: # get here if length of preset matches is too short or market is exhausted seller = random.choice([p.id_in_group for p in self.get_players() if (p.participant.vars['type']=='seller')]) buyer = random.choice([p.id_in_group for p in self.get_players() if (p.participant.vars['type']=='buyer')]) self.proposer=random.choice([buyer,seller]) if self.session.vars['multiresponder']==0: self.active_buyer = buyer self.active_seller = seller self.active_buyer2 = 0 self.active_seller2 = 0 else: self.active_buyer = buyer self.active_seller = seller num_traded = sum([p.traded for p in self.get_players()]) if num_traded == 0: if self.proposer == buyer: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'seller'] if responder_list[0] == seller: self.active_seller2 = responder_list[1] else: self.active_seller2 = responder_list[0] self.active_buyer2 = 0 else: responder_list = [p.id_in_group for p in self.get_players() if p.participant.vars['type'] == 'buyer'] if responder_list[0] == buyer: self.active_buyer2 = responder_list[1] else: self.active_buyer2 = responder_list[0] self.active_seller2 = 0 else: self.active_buyer2 = 0 self.active_seller2 = 0 # create the list of future matches to be displayed def create_matchlist(self,id): b1 = 'B' + str(self.session.vars['value_cost'][0]) s2 = 'S' + str(self.session.vars['value_cost'][1]) b3 = 'B' + str(self.session.vars['value_cost'][2]) s4 = 'S' + str(self.session.vars['value_cost'][3]) b1star = '(' + b1 + ')' s2star = '(' + s2 + ')' b3star = '(' + b3 + ')' s4star = '(' + s4 + ')' # replace id with 'You' if id == 1: b1 = 'You' elif id == 2: s2 = 'You' elif id == 3: b3 = 'You' else: s4 = 'You' # get matches for this period and on, excluding those that have traded validmatches, validproposers = self.exclude_matches() num_traded = sum([p.traded for p in self.get_players()]) if num_traded == 0: showsecondresponders = 1 else: showsecondresponders = 0 proposers = [] responders = [] secondresponders = [] for m in range(1, len(validmatches) - 1): # start at 1 to skip current period if validmatches[m] == 1: if validproposers[m] == 1: proposers.append(b1) responders.append(s4) secondresponders.append(s2star) else: proposers.append(s4) responders.append(b1) secondresponders.append(b3star) elif validmatches[m] == 2: if validproposers[m] == 1: proposers.append(b1) responders.append(s2) secondresponders.append(s4star) else: proposers.append(s2) responders.append(b1) secondresponders.append(b3star) elif validmatches[m] == 3: if validproposers[m] == 1: proposers.append(b3) responders.append(s4) secondresponders.append(s2star) else: proposers.append(s4) responders.append(b3) secondresponders.append(b1star) elif validmatches[m] == 4: if validproposers[m] == 4: proposers.append(b3) responders.append(s2) secondresponders.append(s4star) else: proposers.append(s2) responders.append(b3) secondresponders.append(b1star) return proposers, responders, secondresponders, showsecondresponders # #### Can the active traders in this market trade? # def no_trade(self): # return (self.someone_traded() or self.losing_trade()) # # #### Has one of the active players traded already? # def someone_traded(self): # active_p=[p.traded for p in self.get_players() if p.active()] # if active_p: # return sum(active_p)>0 # else: # return False # # #### Is the active buyer's value lower than the active seller's cost? # def losing_trade(self): # if self.active_seller: # return self.session.vars['value_cost'][int(self.active_seller) - 1]>self.session.vars['value_cost'][int(self.active_buyer) - 1] # else: # return False #### did a player in the group kill trade? def trade_killed(self): return sum([p.kill_trade for p in self.get_players()])>0 #### Is this round a final round for the market? def market_ends(self): return self.trade_killed() or self.market_exhausted() #### Have all of the possible mutually beneficial trades for the market already taken place? def market_exhausted(self): num_traded = sum([p.traded for p in self.get_players()]) if num_traded == Constants.players_per_group: return True elif num_traded == Constants.players_per_group - 2: value = [p.value_cost() for p in self.get_players() if p.participant.vars['type'] == 'buyer' and p.traded == False] cost = [p.value_cost() for p in self.get_players() if p.participant.vars['type'] == 'seller' and p.traded == False] return cost[0] > value[0] else: return False #### Are there only two players left in market so I should allow someone to kill trade? def show_kill_trade(self): x=sum([p.traded for p in self.get_players()]) return (x==Constants.players_per_group-2) def get_channel_group_name(self): return 'all' # def get_period(self): # if len(self.donemessages.all())>0: # return self.donemessages.all()[0].period # else: # return 1 class Player(BasePlayer): # Have you traded yet during this market? traded=models.BooleanField(initial=False) # What price did you trade at? trading_price=models.IntegerField() # player accepted offer? offer_accepted = models.BooleanField() # player terminated trade during a final pairing? kill_trade = models.BooleanField(initial=False) #### Set your payoffs def set_payoffs(self): if self.traded: # if self.participant.vars['role']=='buyer': if self.role()=='buyer': self.payoff=self.value_cost()-self.trading_price self.participant.vars['market_earn'] = self.payoff else: self.payoff=self.trading_price-self.value_cost() self.participant.vars['market_earn'] = self.payoff else: self.payoff=0 #### Get your value/cost def value_cost(self): return self.session.vars['value_cost'][self.id_in_group-1] #### Are you active this round? def active(self): return (self.group.active_buyer==self.id_in_group) or (self.group.active_seller==self.id_in_group or self.group.active_buyer2==self.id_in_group) or (self.group.active_seller2==self.id_in_group) #### Are you the proposer this round? def proposing(self): return (self.group.proposer==self.id_in_group) #### Are you the responder this round? def responding(self): return self.group.proposer!=self.id_in_group and self.active() #### Display Id for proposer def proposer_id(self): if self.group.proposer==self.group.active_buyer: return 'B'+ str(self.session.vars['value_cost'][self.group.active_buyer-1]) else: return 'S' + str(self.session.vars['value_cost'][self.group.active_seller - 1]) #### Display Id for responder #def responder_id(self): # if self.group.proposer!=self.group.active_buyer: # return 'B'+ str(self.session.vars['value_cost'][self.group.active_buyer-1]) # else: # return 'S' + str(self.session.vars['value_cost'][self.group.active_seller - 1]) #### Role for responder def responder_role(self): if self.group.proposer==self.group.active_buyer: return 'seller' else: return 'buyer' #### Role for proposer def proposer_role(self): if self.group.proposer==self.group.active_buyer: return 'buyer' else: return 'seller' def role(self): return self.participant.vars['type'] def get_another(self): return self.get_others_in_group()[0] # class DoneMessage(djmodels.Model): # class Meta: # ordering = ['-created_at'] # # group = djmodels.ForeignKey(to=Group, related_name='donemessages') # period = models.IntegerField(initial=1) # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # # def as_dict(self): # return {'group_name': self.group.get_channel_group_name()}