from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range ) import random, re, datetime author = 'Naiim Mason' doc = """ Your app description """ class Constants(BaseConstants): name_in_url = 'bargaining_experiment' players_per_group = 4 num_rounds = 18 experimental_rounds = 4 bargaining_time_secs = 150 results_time_secs = 30 income = 100 tenant_budget = 400 low_offer_cost = 10 high_offer_cost = 30 donation = 4 negotiations_cost = 25 land_owner_provision = 1 practice_round = 'Practice' insufficient_balance_message = "You do not have enough funds to make this offer" end_negotiations_signal = "Sentinel" end_negotiations_message_HTML = "This round has ended" tenant_role = "Tenant" land_owner_role = "Land Owner" accept_offer = "Accepted" reject_offer = "Rejected" unaccepted_offer = "Pending" null_role = "No role has been set" channel_template = '%d-%d_%s-%d' key_template = 'S:%sD:%s@%s' channel_key = "channel" session_key = "session" offer_cost_key = "offer_cost" recognition_key = "recognition" tenant_channel = "tenant" backup_reload_signal = 'Reload' backup_request_signal = 'Request' message_enqueue_signal = 'Enqueue' message_dequeue_signal = 'Dequeue' message_delivered_signal = 'Delivered' offer_signal = 'offer' acknowledgment_signal = 'acknowledgment' offer_made_by_self = 'You' total_pending_offers_label = 'Number of offers you are considering = ' total_pending_offers_amount_label = 'Total value of considered offers = ' #remaining_budget_label = 'Total earnings in this round if these offers are accepted = ' any_offer_accepted_label = 'Have any offers been accepted in this round yet? ' total_accepted_label = 'Total value of accepted offers = ' @staticmethod def get_time(): now = datetime.datetime.now() month = now.strftime("%B") day = now.strftime("%d") year = now.strftime("%Y") time = now.strftime("%X") timestamp = '%s %s, %s %s' % (month, day, year, time) return timestamp @staticmethod def get_offer_cost(session): return Constants.session_matrix[session][Constants.offer_cost_key] session_matrix = {1: {offer_cost_key: low_offer_cost, recognition_key: False}, 2: {offer_cost_key: high_offer_cost, recognition_key: False}, 3: {offer_cost_key: low_offer_cost, recognition_key: True}, 4: {offer_cost_key: high_offer_cost, recognition_key: True}} class Subsession(BaseSubsession): def get_land_owners(self): if not self.session.vars.__contains__('land_owners_matrix'): land_owners_list = [player for player in self.get_players() if player.role() == Constants.land_owner_role] random.shuffle(land_owners_list) self.session.vars['land_owners_order'] = {} self.session.vars['land_owners_matrix'] = {} counter = 1 while len(land_owners_list) > 0: self.session.vars['land_owners_matrix'][counter] = [] for iteration in range(1, Constants.players_per_group): land_owner = land_owners_list.pop() land_owner.participant.vars['constant_group_id'] = counter self.session.vars['land_owners_matrix'][counter].append(land_owner) index = self.session.vars['land_owners_matrix'][counter].index(land_owner) self.session.vars['land_owners_order'][str(land_owner.participant.id_in_session)] = [counter, index] land_owner.participant.vars['constant_order'] = index counter += 1 else: land_owners_list = [player for player in self.get_players() if player.role() == Constants.land_owner_role] for land_owner in land_owners_list: group_key = self.session.vars['land_owners_order'][str(land_owner.participant.id_in_session)][0] order_key = self.session.vars['land_owners_order'][str(land_owner.participant.id_in_session)][1] self.session.vars['land_owners_matrix'][group_key][order_key] = land_owner return self.session.vars['land_owners_matrix'] def creating_session(self): if ((self.round_number-3) == -2) or ((self.round_number-3) % Constants.experimental_rounds == 0): tenants = [player for player in self.get_players() if player.role() == Constants.tenant_role] land_owners = self.get_land_owners() group_matrix = [] counter = 1 session = 0 if (self.round_number-3) == -2 else (self.round_number-3)//4 for rotation in range(0, session): tenants.append(tenants.pop(0)) while len(tenants) > 0: new_group = [tenants.pop()] new_group.extend(land_owners[counter]) group_matrix.append(new_group) counter += 1 self.set_group_matrix(group_matrix) elif (self.round_number-3) == -1: self.group_like_round(1) else: self.group_like_round(3 + ((self.round_number - 3) - ((self.round_number - 3) % Constants.experimental_rounds))) # Be careful with external objects. Whenever the code is edited while the server is running, # all objects on players, groups, and subsessions will be reinitialized class Group(BaseGroup): terminate = models.BooleanField(initial=False) tenant_id = models.IntegerField() contract_message = models.StringField() constant_group_id = models.IntegerField() accepted_offers_amount = models.IntegerField() total_offers_made = models.IntegerField(initial=0) contract_negotiated = models.BooleanField(initial=False) tenant_balance = models.IntegerField(initial=Constants.tenant_budget) offers_pending_response = models.IntegerField(initial=0) total_offers_accepted = models.IntegerField(initial=0) remaining_budget = models.IntegerField() is_group_negotiating = models.BooleanField(initial=True) pending_processing = models.BooleanField() donation = models.IntegerField() def get_player_by_sid(self, id_in_session): for player in self.get_players(): if player.participant.id_in_session == id_in_session: return player raise Exception('Player with id %d not found' % id_in_session) def start_group(self): for player in self.get_players(): if player.role() == Constants.land_owner_role: self.constant_group_id = player.participant.vars['constant_group_id'] if not player.is_negotiating: self.is_group_negotiating = False for player in self.get_players(): player.start_player() self.pending_processing = self.is_group_negotiating def terminate_negotiations(self): self.terminate = True for player in self.get_players(): player.negotiations_terminated = True player.save() def submit_form(self, time): if self.contract_negotiated or \ (time < 1 and (self.offers_pending_response == 0 or self.terminate)): for player in self.get_players(): player.submit_form = True player.save() print('Submitting Form') return True else: return False def reload_backup(self, player_id): return self.get_player_by_sid(player_id).participant.vars def get_id_from_channel(self, channel): if channel == Constants.tenant_channel: return self.get_player_by_role(Constants.tenant_role).participant.id_in_session for player in self.get_players(): if player.owns_channel(channel): return player.participant.id_in_session raise Exception('Invalid channel found: ' + channel) def make_offer(self, catalogue_code, source_id, amount): catalogue_id = self.get_id_from_channel(catalogue_code) if int(source_id) == catalogue_id: source_role = Constants.land_owner_role destination = self.get_tenant_id() else: source_role = Constants.tenant_role destination = catalogue_id self.get_player_by_sid(catalogue_id).log_offer(catalogue_id, source_role, int(amount)) self.get_player_by_sid(self.get_tenant_id()).log_offer(catalogue_id, source_role, int(amount)) self.total_offers_made += 1 self.offers_pending_response += 1 self.session.save() print('Offer | Group: ' + str(self.constant_group_id) + ' From: ' + str(source_role) + ' To: ' + str(destination) + ' Amount: ' + str(amount) + ' Offers Pending: ' + str(self.offers_pending_response)) def acknowledge_offer(self, catalogue_code, reply): catalogue_id = self.get_id_from_channel(catalogue_code) self.get_player_by_sid(catalogue_id).log_reply(catalogue_id, reply) self.get_player_by_sid(self.get_tenant_id()).log_reply(catalogue_id, reply) self.offers_pending_response -= 1 if reply == Constants.accept_offer: self.total_offers_accepted += 1 self.contract_negotiated = self.total_offers_accepted == 3 self.tenant_balance -= self.get_player_by_sid(catalogue_id).get_accepted_offer() self.session.save() print('Reply | Group: ' + str(self.constant_group_id) + ' Catalogue: ' + str(catalogue_id) + ' Budget: ' + str(self.tenant_balance) + ' Reply: ' + reply + ' Offers Pending: ' + str(self.offers_pending_response)) def post_process(self): print('Starting post processing for group %d' % self.constant_group_id) self.tenant_id = self.get_tenant_id() self.contract_message = 'Yes' if self.contract_negotiated else 'No' for player in self.get_players(): if player.role() == Constants.tenant_role: player.total_amount_of_accepted_offers = player.get_total_accepted_offers() earnings = self.tenant_balance if self.contract_negotiated else 0 self.remaining_budget = self.tenant_balance if self.contract_negotiated else 0 print('Player %d with tenant role\n%d + %d + %d' % (player.participant.id_in_session, Constants.income, earnings, player.negotiations_cost)) else: player.amount_of_accepted_offer = player.get_accepted_offer() earnings = player.amount_of_accepted_offer if self.contract_negotiated else 0 print('Player %d with land owner role\n%d + %d + %d' % (player.participant.id_in_session, Constants.income, earnings, player.negotiations_cost)) player.new_donation = self.contract_negotiated player.balance += Constants.income + earnings - player.negotiations_cost if self.contract_negotiated: self.donation = Constants.donation if self.round_number != 1 and player.in_round(player.round_number-1).get_round() != 'Practice': player.donations_received = player.in_round(player.round_number-1).donations_received + 1 elif self.round_number == 1 or self.round_number == 3: player.donations_received = 1 else: self.donation = 0 if self.round_number != 1 and player.in_round(player.round_number-1).get_round() != 'Practice': player.donations_received = player.in_round(player.round_number-1).donations_received elif self.round_number == 1 or self.round_number == 3: player.donations_received = 0 if player.get_round() != Constants.practice_round: player.participant.payoff += player.balance player.participant.vars['donations'] = player.donations_received print('ID: ' + str(player.participant.id_in_session) + ' Negotiation Cost: ' + str(player.negotiations_cost) + ' Earnings: ' + str(earnings) + ' Balance: ' + str(player.balance) + ' Payoff: ' + str(player.participant.payoff) + ' Budget: ' + str(self.remaining_budget) + ' Round: ' + str(player.get_round())) self.pending_processing = False def get_land_owner_order(self, land_owner_id): return self.get_land_owner_ids().index(land_owner_id)+1 def get_tenant_id(self): return self.get_player_by_role(Constants.tenant_role).participant.id_in_session def get_land_owner_channels(self): land_owner_channels = [] for player in self.get_players(): if player.role() == Constants.land_owner_role: land_owner_channels.append(player.get_channel()) if len(land_owner_channels) != Constants.players_per_group-1: raise Exception('Missing channel for land owner') return land_owner_channels def get_land_owner_ids(self): land_owner_ids = [] for player in self.get_players(): if player.role() == Constants.land_owner_role: land_owner_ids.append(player.participant.id_in_session) if len(land_owner_ids) != Constants.players_per_group-1: raise Exception('Missing id for land owner') return land_owner_ids class Player(BasePlayer): balance = models.IntegerField(initial=0) donations_received = models.IntegerField(initial=0) new_donation = models.BooleanField(initial=False) offer_was_accepted = models.BooleanField(initial=False) number_of_offers = models.IntegerField(initial=0) total_amount_of_accepted_offers = models.IntegerField() amount_of_accepted_offer = models.IntegerField() timestamp = models.StringField() has_backup_data = models.BooleanField(initial=False) negotiations_cost = models.IntegerField(initial=0) is_negotiating = models.BooleanField(initial=True) negotiations_terminated = models.BooleanField(initial=False) submit_form = models.BooleanField(initial=False) data_dump = models.StringField() current_offer = models.StringField() order = models.IntegerField(initial=1) player_initialized = models.BooleanField(initial=False) def get_round(self): return Constants.practice_round if self.round_number < 3 else self.round_number - 2 def get_negotiations_cost(self): return 3*Constants.negotiations_cost if self.role() == Constants.tenant_role else Constants.negotiations_cost def start_player(self): if self.player_initialized: print('Player %d already initialized' % self.participant.id_in_session) return None if self.group.is_group_negotiating: self.negotiations_cost = self.get_negotiations_cost() self.player_initialized = True for player_id in self.group.get_land_owner_ids(): self.participant.vars[player_id] = {} self.participant.vars[player_id]['offers'] = [] self.participant.vars[player_id]['accepted_offer'] = None self.participant.vars[player_id]['offer_was_accepted'] = False self.timestamp = Constants.get_time() print('Player %d started in round %s' % (self.participant.id_in_session, str(self.get_round()))) def end_player(self): self.data_dump = str(self.participant.vars) for player_id in self.group.get_land_owner_ids(): self.participant.vars[player_id].clear() def has_pending_offers(self, player_id): has_pending = False if len(self.participant.vars[player_id]['offers']) > 0: current_offer = self.participant.vars[player_id]['offers'][-1] has_pending = current_offer['Reply'] == Constants.unaccepted_offer print('Current offer for player %d: %s' % (self.participant.id_in_session, str(current_offer))) return has_pending def log_offer(self, catalogue_id, source_role, offer_amount): if self.has_pending_offers(catalogue_id): raise Exception('Player already has a pending offer') self.has_backup_data = True self.number_of_offers += 1 offer = { 'ID': self.participant.id_in_session, 'BackupData': None, 'Land_Owner': self.group.get_player_by_sid(catalogue_id).participant.vars['constant_order']+1, 'Source': Constants.offer_made_by_self if source_role == self.role() else source_role, 'Offer': offer_amount, 'Reply': Constants.unaccepted_offer, 'Number': len(self.participant.vars[catalogue_id]['offers'])+1, 'sourceGroup': self.group.id } self.current_offer = str(offer) self.participant.vars[catalogue_id]['offers'].append(offer) self.participant.save() self.save() print('Offer %d logged for player %d' % (offer_amount, self.participant.id_in_session)) def log_reply(self, catalogue_id, reply): if len(self.participant.vars[catalogue_id]['offers']) == 0: data = str(self.participant.vars.get(catalogue_id)) print(self.participant.vars.keys()) raise Exception('Cannot find offer to respond to for player %d searching catalogue %d: %s' % (self.participant.id_in_session, catalogue_id, data)) self.participant.vars[catalogue_id]['offers'][-1]['Reply'] = reply if reply == Constants.accept_offer: self.participant.vars[catalogue_id]['accepted_offer'] = self.participant.vars[catalogue_id]['offers'][-1]['Offer'] self.participant.vars[catalogue_id]['offer_was_accepted'] = True self.offer_was_accepted = True self.participant.save() self.save() print('Reply "%s logged for player %d' % (reply, self.participant.id_in_session)) def get_land_owner_accepted_offer(self): accepted_offers = [] for land_owner_id in self.group.get_land_owner_ids(): accepted_offers.append(self.group.get_player_by_sid(land_owner_id).amount_of_accepted_offer) return accepted_offers def get_land_owner_total_offers(self): total_offers = [] for land_owner_id in self.group.get_land_owner_ids(): total_offers.append(self.group.get_player_by_sid(land_owner_id).number_of_offers) return total_offers def get_accepted_offer(self): return self.participant.vars[self.participant.id_in_session]['accepted_offer'] def get_total_accepted_offers(self): lo_ids = self.group.get_land_owner_ids() history = self.participant.vars ao = 'accepted_offer' total_accepted_offers = [history[lo_id][ao] for lo_id in lo_ids if history[lo_id][ao] is not None] print('Total accepted offers: ' + str(total_accepted_offers)) return None if len(total_accepted_offers) == 0 else sum(total_accepted_offers) def create_channel(self): print('Creating channel for participant: ' + str(self.participant.id_in_session)) if self.role() == Constants.land_owner_role: self.participant.vars[Constants.channel_key] = Constants.channel_template % (self.group.get_tenant_id(), self.participant.id_in_session, '%d', self.group.id) def get_channel(self): if self.role() == Constants.tenant_role: return Constants.tenant_channel if not self.participant.vars.__contains__(Constants.channel_key): self.create_channel() return self.participant.vars[Constants.channel_key] % self.round_number def owns_channel(self, channel): if channel == Constants.tenant_channel and self.role() == Constants.tenant_role: return True embedded_id = re.search(r'-[0-9]*_', channel).group().strip('-_') return self.participant.id_in_session == int(embedded_id) def role(self): if (self.participant.id_in_session-1) % 4 == 0: return Constants.tenant_role else: return Constants.land_owner_role