from typing import Tuple from django.forms.widgets import ChoiceWidget from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer ) class TrueFalseWidget(ChoiceWidget): input_type = 'radio' template_name = 'scrutiny/widget/true_false.html' def __init__(self, attrs=None): super().__init__(attrs) self.choices = [(True, 'True'), (False, 'False')] class Constants(BaseConstants): name_in_url = 'scrutiny' players_per_group = None num_rounds = 10 # The build-in roles functionality is rubbish blue = 'BLUE' green = 'GREEN' up = 'UP' middle = 'MIDDLE' down = 'DOWN' left = 'LEFT' right = 'RIGHT' stranger = 'STRANGER' acquaintance = 'ACQUAINTANCE' friend = 'FRIEND' # state -> report -> choice payoffs = { up: { up: {left: (50, 50), right: (40, 40)}, middle: {left: (40, 30), right: (30, 40)} }, middle: { up: {left: (40, 30), right: (30, 40)}, middle: {left: (50, 50), right: (40, 40)}, down: {left: (10, 30), right: (0, 40)}, }, down: { middle: {left: (40, 30), right: (30, 40)}, down: {left: (20, 50), right: (10, 40)}, }, } # States are hard coded states = [down, middle, down, up, middle, middle, up, down, down, up] @classmethod def get_payoffs(cls, state: str, report: str, choice: str) \ -> Tuple[int, int]: return cls.payoffs.get(state, {}).get(report, {}).get(choice, (0, 0)) class Subsession(BaseSubsession): @staticmethod def label(idx: int) -> str: if idx % 2 == 0: return f"B{int(idx / 2)}" else: return f"G{int((idx + 1) / 2)}" @staticmethod def color(idx: int) -> str: if idx % 2 == 0: return Constants.blue else: return Constants.green def creating_session(self): if self.round_number == 1: greens, blues = [], [] for player in self.get_players(): player.participant.label = self.label(player.id_in_group) if player.id_in_group % 2 == 0: blues.append(player.id_in_group) else: greens.append(player.id_in_group) assert len(greens) == len(blues) self.session.config['rounds'] = {} for round_idx in range(1, Constants.num_rounds + 1): self.session.config['rounds'][round_idx] = [ (green, blues[idx]) for idx, green in enumerate(greens)] # Rotate to provide different matches blues = blues[1:] + [blues[0]] # Copy for each round group = self.get_groups()[0] state = Constants.states[self.round_number - 1] for green_id, blue_id in \ self.session.config['rounds'][self.round_number]: group.get_player_by_id(green_id).set_attributes(blue_id, state) group.get_player_by_id(blue_id).set_attributes(green_id, state) class Group(BaseGroup): def set_payoffs(self): for player in self.get_players(): if player.color == Constants.blue: report = player.decision choice = player.other.decision payoff = Constants.get_payoffs(player.state, report, choice)[0] else: report = player.other.decision choice = player.decision payoff = Constants.get_payoffs(player.state, report, choice)[1] player.payoff = payoff class Player(BasePlayer): sona_id = models.LongStringField(widget=widgets.TextInput) color = models.LongStringField() # The 'id_in_group' of the opponent in this round other_id = models.IntegerField() other_label = models.LongStringField() state = models.LongStringField() decision = models.LongStringField(blank=True) judgement = models.LongStringField() # These are the fields for the introduction questions compensated = models.BooleanField(widget=TrueFalseWidget, label='') overview = models.BooleanField(widget=TrueFalseWidget, label='') procedures_1 = models.BooleanField(widget=TrueFalseWidget, label='') procedures_2 = models.BooleanField(widget=TrueFalseWidget, label='') procedures_3 = models.BooleanField(widget=TrueFalseWidget, label='') point_earnings_1 = models.BooleanField(widget=TrueFalseWidget, label='') point_earnings_2 = models.BooleanField(widget=TrueFalseWidget, label='') point_earnings_3 = models.BooleanField(widget=TrueFalseWidget, label='') @property def other(self) -> 'Player': return self.group.get_player_by_id(self.other_id) def set_attributes(self, other_id: int, state: str): self.color = Subsession.color(self.id_in_group) self.other_id = other_id self.other_label = Subsession.label(other_id) self.state = state