from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c , currency_range, ExtraModel, ) import random import time from otree.export import sanitize_for_csv author = 'Huanren Zhang' doc = """ Repeated game, only one match. Dropouts (no response in 1 minute) causes termination for the matched pair. """ class Constants(BaseConstants): name_in_url = 'game' location = 'repeated_game' players_per_group = 2 timer_display_seconds = 31 # timer shows after time left is less than timer_display_seconds results_display_seconds = 8 # time for displaying results in each round; decrease 1 second per 10 rounds # debug = 0 # whether it is debug # timeout_seconds = 3 if debug else 65 # time allowed for making decision -- debug timeout_seconds = 65 # time allowed for making decision -- debug action_labels = ['Y','W'] # labels for the player's actions pmatrix_template = location + '/payoff_matrix.html' ptable_template = location + '/payoff_table.html' history_template = location + '/History.html' historyall_template = location + '/HistoryAllRounds.html' num_rounds = 1 interaction_length = 116 # payoff matrix for each interaction payoff_matrix = { 'PD': [[3,1,4,2],[3,1,4,2]], # [[P11_1,P12_1,P21_1,P22_1],[P11_2,P12_2,P21_2,P22_2]]] 'PD2': [[3,-1,4,2],[3,-1,4,2]], # [[P11_1,P12_1,P21_1,P22_1],[P11_2,P12_2,P21_2,P22_2]]] 'BO': [[1,2,4,1],[1,2,4,1]], 'SH': [[3,0,2,1],[3,0,2,1]], 'CH': [[3,1,4,0],[3,1,4,0]], 'CI': [[3,1,2,0],[3,1,2,0]], 'SD': [[2,2,5,3],[2,3,2,5]], 'SD2': [[1,1,4,2],[1,2,1,6]], 'UM': [[4,1,1,2],[1,2,2,1]], 'UL': [[2,2,3,1],[2,1,2,0]], 'SH2': [[4,0,2,1],[3,0,2,1]], } class Subsession(BaseSubsession): def creating_session(self): # starting_time = time.time() matches = [] interaction_length = self.session.config.get('interaction_length', Constants.interaction_length) self.session.vars['num_groups'] = 0 for p in self.get_players(): p.participant.vars['history'] = [] p.participant.vars['display_timestamp'] = -1 # initialization of time stamp for decision page p.player_id = p.participant.id_in_session for round_number in range(1,interaction_length+1): matches.append(Match(player=p, match_round = round_number)) # Match.objects.create(player=p, match_round=round_number) Match.objects.bulk_create(matches) # print('creating session: there are',Match.objects.count(), ' match objects') # print('seconds used to creating session data:%d'%(time.time() - starting_time)) def group_by_arrival_time_method(self, waiting_players): pmats = self.session.config['pmats'] ## read games to be played from the session config if len(waiting_players) >= Constants.players_per_group: # print('about to create a group') matched_players = waiting_players[:Constants.players_per_group] if isinstance(pmats,str): pmat = pmats else: # pmat = random.choice(pmats) pmat = pmats[self.session.vars['num_groups'] % len(pmats)] for p in matched_players: p.participant.vars['matched'] = 1 p.pmat = pmat self.session.vars['num_groups'] += 1 return matched_players # print(waiting_players,'not enough players yet to create a group') if len(waiting_players)==1: if waiting_players[0].waiting_too_long(): waiting_players[0].participant.vars['matched'] = 0 waiting_players[0].participant.vars['finished'] = 1 waiting_players[0].participant.vars['game_ending'] = 1 return waiting_players players = self.get_players() incoming_players = [p for p in players if p.participant.vars.get('started', 0) and (not p.participant.vars.get('finished', 0)) and (not p.participant.vars.get('matched', 0)) ] print('waiting_playsers',waiting_players,'incoming_players',incoming_players) if (len(incoming_players) == 1): # if there is only one player that cannot be matched p, = incoming_players p.participant.vars['matched'] = 0 p.participant.vars['finished'] = 1 p.participant.vars['game_ending'] = 1 return [p] class Group(BaseGroup): pass class Player(BasePlayer): player_id = models.PositiveIntegerField() partner_id = models.PositiveIntegerField() type = models.IntegerField() # used to define the role of the players, distinguished from the system-defined role pmat = models.StringField() # indicator of the payoff matrix ## the following records the payoff matrix information ## P11,P11o P12,P21o ## P21,P12o P22,P22o P11 = models.IntegerField() P12 = models.IntegerField() P21 = models.IntegerField() P22 = models.IntegerField() P11o = models.IntegerField() P12o = models.IntegerField() P21o = models.IntegerField() P22o = models.IntegerField() decision_time = models.IntegerField(initial=0) # total decision time loading_time = models.FloatField(initial=0) # decision time in seconds player_round = models.IntegerField(initial=1) # current round status = models.StringField(initial='new') # current status of the round: new, submitted, completed def get_partner(self): return self.get_others_in_group()[0] def waiting_too_long(self): return time.time() - self.participant.vars['waitpage_arrival_time'] > self.session.config.get('matching_wait_minutes',5)*60 + 5 def live_method(self, data): # print(data) if data == 'dropout': # if one player dropout self.status = 'dropout' self.participant.vars['finished'] = 1 self.participant.vars['dropout'] = 1 po = self.get_partner() po.status = 'other_dropout' return {0:{'status':'dropout'}} elif data == 'other_dropout': self.status = 'other_dropout' po = self.get_partner() po.status = 'dropout' po.participant.vars['finished'] = 1 po.participant.vars['dropout'] = 1 return {0:{'status':'dropout'}} elif data.get('type') == 'unload': self.participant.vars['time_used'] += data['time_used'] elif data: # submission of actions self.status = 'submitted' self.participant.vars['time_used'] = 0 self.loading_time = max(self.loading_time,round((time.time() - self.participant.vars['display_timestamp'] - data['decision_time'])*10)/10) # print(self.id_in_group,int(data['display_timestamp']), int(self.participant.vars['display_timestamp']),self.loading_time) match = Match.objects.get(player=self, match_round=self.player_round) match.action = data['action'] match.decision_time = int(data['decision_time']*10)/10 # match.decision_time = round(Constants.timeout_seconds - data['time_left']) # print(match.player.player_id,match.player.participant.code,match.match_number,match.action,match.decision_time) match.save() # print('player',self.player_id,', decision time ',match.decision_time) group = self.group players = group.get_players() # all players have submitted actions: group interaction if sum([p.status == 'submitted' for p in players]) == Constants.players_per_group: results_display_seconds = max(Constants.results_display_seconds - match.match_round/10*1.5,3) # timeout_seconds = 3 if self.session.config.get('debug',0) else Constants.timeout_seconds, # time allowed for making decision group_matches = dict() for i,p in enumerate(players): group_matches[i] = dict(player=p,match=Match.objects.get(player=p, match_round=p.player_round)) group_matches[i]['match'].type = p.type group_matches[i]['match'].pmat = p.pmat for i,p in enumerate(players): group_matches[i]['match'].other_action = group_matches[1-i]['match'].action group_matches[i]['match'].other_decision_time = group_matches[1-i]['match'].decision_time group_matches[i]['match'].payoff = [p.P11, p.P12, p.P21, p.P22][ (group_matches[i]['match'].action=='W') * 2 + (group_matches[i]['match'].other_action=='W')] p.payoff += group_matches[i]['match'].payoff group_matches[i]['match'].cum_payoff = p.payoff for i,p in enumerate(players): group_matches[i]['match'].other_payoff = group_matches[1-i]['match'].payoff group_matches[i]['match'].other_cum_payoff = group_matches[1-i]['match'].cum_payoff group_matches[i]['match'].save() m = group_matches[i]['match'] pace = 'normal' if m.match_round > 10: if m.decision_time > 20: pace = 'slow' elif m.decision_time - m.other_decision_time > 12: pace = 'slower' match_info = dict(round_number=m.match_round,action=m.action,other_action=m.other_action, payoff=m.payoff,other_payoff=m.other_payoff,cum_payoff=m.cum_payoff, other_cum_payoff=m.other_cum_payoff,pace=pace) p.participant.vars['history'].append(match_info) p.participant.vars['display_timestamp'] = time.time() + results_display_seconds p.participant.vars['payload'] = match_info.copy() if p.player_round < self.session.config.get('interaction_length',Constants.interaction_length): p.player_round += 1 p.status = 'new' else: p.status = 'finished' p.participant.vars['payload']['status'] = 'finished' p.participant.vars['payload']['round_number'] += 1 # most recent play appears first in the history p.participant.vars['payload']['history'] = p.participant.vars['history'][::-1] p.participant.vars['payload']['time_left'] = Constants.timeout_seconds p.participant.vars['payload']['results_display_seconds'] = results_display_seconds # p.participant.vars['payload']['display_timestamp'] = p.participant.vars['display_timestamp'] payload = dict() for p in players: payload[p.id_in_group] = p.participant.vars['payload'] return payload else: return po = self.get_partner() if po.status == 'dropout': # if one player dropout self.status = 'other_dropout' return {0:{'status':'dropout'}} # if (self.player_round == 1): # # and(self.participant.vars.get('display_timestamp',-1) == -1): # self.participant.vars['display_timestamp'] = time.time() # time_left = Constants.timeout_seconds # # print('initialization of timeout seconds') # else: # time_left = int(Constants.timeout_seconds - (time.time() - self.participant.vars['display_timestamp'])) self.participant.vars['display_timestamp'] = time.time() time_left = Constants.timeout_seconds - self.participant.vars['time_used'] match = Match.objects.get(player=self, match_round=self.player_round) payload = dict(round_number=match.match_round, status=self.status, action=match.action, time_left=time_left, # display_timestamp = self.participant.vars['display_timestamp'], history=self.participant.vars['history'][::-1]) return {self.id_in_group: payload} class Match(ExtraModel): player = models.Link(Player) match_round = models.IntegerField() action = models.StringField() payoff = models.CurrencyField() cum_payoff = models.CurrencyField() decision_time = models.FloatField() # decision time in seconds other_action = models.StringField() other_payoff = models.CurrencyField() other_cum_payoff = models.CurrencyField() other_decision_time = models.FloatField() # decision time in seconds def custom_export(players): print('Customr export: there are', Match.objects.count(), 'match objects') # starting_time = time.time() yield ['session', 'participant','group','game','type','round','action','payoff','decision_time','cum_payoff', 'other_action', 'other_payoff', 'other_decision_time', 'other_cum_payoff'] fields_to_export = ['match_round','action','payoff','decision_time','cum_payoff', 'other_action', 'other_payoff', 'other_decision_time', 'other_cum_payoff',] for p in players: row_start = [ p.session.code, p.participant.code, p.group_id, p.pmat, p.type,] matches = Match.objects.filter(player=p) for match in matches: if not(match.other_action is None): yield row_start + [sanitize_for_csv(getattr(match, f)) for f in fields_to_export] # for match in Match.objects.all(): # yield [match.player.session.code, match.player.participant.code, # match.player.group_id, match.player.pmat,match.player.type]\ # + [sanitize_for_csv(getattr(match, f)) for f in fields_to_export] # print('seconds used to export data:%d'%(time.time() - starting_time))