import plistlib import random from otree import settings from otree.api import * import time from . import task_matrix from .image_utils import encode_image doc = """ Your app description """ def creating_session(subsession): subsession.TREATMENT = 'T1' #damit es nicht Null ist if 'wb_treatment' in subsession.session.config: subsession.TREATMENT = subsession.session.config['wb_treatment'] #T1: message wb/non embezzle; T2: message remaining silent/ embezzle; T3: message wb + non-embezzle + remaining silent + embezzle for p in subsession.get_players(): p.participant.vars['playertype'] = '' #config variable setzen subsession.matched_new = False class C(BaseConstants): NAME_IN_URL = 'WB_Norm_Appropriateness_2' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 #wenn XXX_ROLE eingefügt wird, legt Otree automatisch eine Variable role auf player Ebene an A_ROLE = 'A' B_ROLE = 'B' ## Definition : Gruppe1 A B & Gruppe2 A B werden zu ## Spieler1 G1A Spieler2 G2A ## Spieler1 G1B Spieler2 G2B Player_1 = 1 Player_2 = 2 DONATION_BUDGET = cu(1.5) EMBEZZLE_BUDGET = cu(2) EMBEZZLE_BUDGET_B = cu(1) EMBEZZLE_GROUP_BUDGET = EMBEZZLE_BUDGET_B + EMBEZZLE_BUDGET PENALTY_BUDGET_A = cu(1) PENALTY_BUDGET_B = cu(0.5) ESTIMATE_CORRECT_WB = cu(0.25) # wenn jemand bei der Einschätzung für WB richtig liegt ESTIMATE_CORRECT_SILENT = cu(0.25) # wenn jemand bei der Einschätzung für Silent richtig liegt TEAM_PAYOFF = cu(1.5) SOLO_PAYOFF = cu(1.2) HIGH_RETALIATION = cu(5) LOW_RETALIATION = cu(1) DONATION_AFTER_EMBEZZLING = DONATION_BUDGET - EMBEZZLE_BUDGET ## nur für die anzeige DONATION_DECISION_A = __name__ + '/donation_a.html' DONATION_DECISION_B = __name__ + '/donation_b.html' DONATION_FORWARDED = __name__ + '/decision_forward.html' DONATION_SILENCED = __name__ + '/decision_silenced.html' DONATION_REPORTED = __name__ + '/decision_report.html' #TREATMENTS_NAMES = ['Control','T1','T2'] TREATMENTS_NAMES = ['Low_Retaliation','High_Retaliation'] class Subsession(BaseSubsession): amount_possible_donations = models.IntegerField(initial=0) amount_real_donations = models.IntegerField(initial=0) amount_embezzling_tries = models.IntegerField(initial=0) amount_embezzling_succeeds = models.FloatField(initial=0) ## hier noch die anzahl TREATMENT = models.StringField(initial='Low_Retaliation') total_donation = models.FloatField(initial=0.0) ## TODO Sabrina, wenn du das nicht brauzchst, lösch ruhi die Felder ##Mehrheit embezzle/non embezzle und wb/non-wb #for Role A: tie=0, embezzle= 1, non-embezzle= 2 #for Role B: tie=0, wb = 1, non-wb=2 #both initial = -1 majority_A = models.IntegerField (initial=-1) majority_B = models.IntegerField(initial=-1) matched_new = models.BooleanField (initial=False) #für T2: ob Mehrheit WB/nonWB oder Embez/nonEmbez macht majority_AT2 = models.IntegerField(initial=-1) majority_BT2 = models.IntegerField(initial=-1) class Group(BaseGroup): donation_amount = models.CurrencyField(initial=cu(0)) class Player(BasePlayer): FirstPartA = models.BooleanField(initial=False) #für die fixe Rollenzuweisung playertype = models.StringField() donation_b_silence = models.BooleanField( label="", choices=[ [False, "Report the behaviour of Player A"], [True, "Overlook the behaviour of Player A"] ], widget=widgets.RadioSelectHorizontal, initial= True if settings.DEBUG else None ) donation_a = models.BooleanField( label="", choices = [ [True, 'Transfer the full donation budget to Cancer Research UK '], [False, 'Keep part of the donation budget for yourself '] ], widget=widgets.RadioSelectHorizontal, initial=True if settings.DEBUG else None ) embezzlingbudget_A = models.IntegerField( label="How appropriate do you personally belief is it to keep part of the donation budget for yourself?", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) forwardingbudget_A = models.IntegerField( label="How appropriate do you personally believe is it to transfer the full donation budget to Cancer Research UK?", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) majority_embezzlingbudget_A = models.IntegerField( label="Which option do you think the majority of participants chose in the preceding question? If your answer matches the actual option of the majority you will earn an additional 0.25 GPB.", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) majority_forwardingbudget_A = models.IntegerField( label="Which option do you think the majority of participants chose in the preceding question? If your answer matches the actual option of the majority you will earn an additional 0.25 GPB.", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) wb_B = models.IntegerField( label="How appropriate do you personally belief is it to report the behaviour of Player A?", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) remainingsilent_B = models.IntegerField( label="How appropriate do you personally belief is it to overlook the behaviour of Player A?", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial = 2 if settings.DEBUG else -1 ) majority_wb_B = models.IntegerField( label="Which option do you think the majority of participants chose in the preceding question? If your answer matches the actual option of the majority you will earn an additional 0.25 GPB.", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) majority_remainingsilent_B = models.IntegerField( label="Which option do you think the majority of participants chose in the preceding question? If your answer matches the actual option of the majority you will earn an additional 0.25 GPB.", choices=[ [1, 'very inappropriate'], [2, 'somewhat inappropriate'], [3, 'somewhat appropriate'], [4, 'very appropriate'], ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) #deskriptive Norm für T0 und T1 ohne zusätzliches Geld majority_action_A = models.IntegerField( label="Unabhängig davon, was glauben Sie, wie hat sich die Mehrheit der Spieler*in A verhalten?", choices=[ [1, 'Das vollständige Spendenbudget an GoAhead weiterzuleiten'], [2, 'Einen Teil des Spendenbudgets einzubehalten'] ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) majority_action_B = models.IntegerField( label="Unabhängig davon, was glauben Sie, wie hat sich die Mehrheit der Spieler*in B verhalten?", choices=[ [1, 'Das Verhalten von Spieler*in A zu melden'], [2, 'Stillschweigend über das Verhalten von Spieler*in A hinwegzusehen'] ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) #T2 majority_view_honesty_AT2 = models.IntegerField( label="Geben Sie an, was Sie denken, wie sich die Mehrheit der Teilnehmenden in der Rolle 'Spieler*in A' in dieser Sitzung bei der Entscheidung über das Spendenbudget verhalten hat. Wenn Ihre Einschätzung bei dieser Frage korrekt ist, erhalten Sie eine zusätzliche Auszahlung von 1 Taler.", choices=[ [1, 'Das vollständige Spendenbudget an GoAhead weiterleiten'], [2, 'Einen Teil des Spendenbudgets einbehalten'] ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) majority_view_wb_BT2 = models.IntegerField( label="Geben Sie an, was Sie denken, wie sich die Mehrheit der Teilnehmenden in der Rolle 'Spieler*in B' in dieser Sitzung bezüglich der Entscheidung von Spieler*in A verhalten hat. Wenn Ihre Einschätzung bei dieser Frage korrekt ist, erhalten Sie eine zusätzliche Auszahlung von 1 Taler.", choices=[ [1, 'Das Verhalten von Spieler*in A melden'], [2, 'Stillschweigend über das Verhalten von Spieler*in A hinwegsehen'] ], widget=widgets.RadioSelectHorizontal, initial=2 if settings.DEBUG else -1 ) Spieler1 = models.BooleanField(initial=False) #ob jemand Spieler 1 oder 2 wird team_embezzle = models.BooleanField(initial=False) ## Nur für eure Auswertung und das leichtere Überprüfen team_penalty = models.BooleanField(initial=False) estimate_correct = models.IntegerField (initial=-1) #wird gesetzt aber für Experiment Norm_Appropriateness nicht gebraucht def fill_control_variables_set_payoff(group:Group): ## Spieler 1 ist A , Spieler 2 ist B , wenn ihr später rnd zuordnung macht evtl nochmal anfassen ## falls dem so ist, die 0 & 1 unten durch die suche nach A und B tauschen ## index = 0 if group.get_player()[0].role == C.A_ROLE else 1 ## und dann 0 und 1 durch index und 1-index ersetzen #determine who is who player1A = True if group.get_players()[0].role == C.B_ROLE: player1A = False embezzle = False report = True if player1A: #A Spieler ist an Stelle 0 group.get_players()[0].donation_b_silence = group.get_players()[1].donation_b_silence group.get_players()[1].donation_a = group.get_players()[0].donation_a if group.get_players()[0].donation_a == False: embezzle=True if group.get_players()[0].donation_b_silence: report = False else: #B Spieler ist an Stelle 0 group.get_players()[1].donation_b_silence = group.get_players()[0].donation_b_silence group.get_players()[0].donation_a = group.get_players()[1].donation_a if group.get_players()[0].donation_a==False: embezzle = True if group.get_players()[0].donation_b_silence: report = False group.donation_amount = C.DONATION_BUDGET for player in group.get_players(): player.payoff = cu(0) ## kriegen erstmal nix player.team_embezzle = embezzle player.team_penalty = embezzle and report #für Experiment WB Norm Appropriateness: speichern in participant variable player.participant.vars['player_embezzle'] = embezzle player.participant.vars['player_wb'] = report #um nullwerte zu vermeiden werden die Variablen beider Spieler belegt #if player.id_in_group == 1: #player.donation_b_silence = report #else: #ID ist 2 #.donation_a=embezzle #print("Gruppe" + player.group + " , embezzle: " + embezzle + ", wb: " + report) ## nur zum zählen if embezzle and not report: player.subsession.amount_embezzling_succeeds += 1/2 ## wird ja 2mal gezählt ## HIER PAYOFF if embezzle: ## nur wenn unterschlagen wird passiert überhaupt was if report: ## und gemeldet player.payoff -= C.LOW_RETALIATION if player.subsession.TREATMENT == C.TREATMENTS_NAMES[0] else C.HIGH_RETALIATION else: ## stillschweigend if player.role==C.A_ROLE: player.payoff = C.EMBEZZLE_BUDGET else: ## Spieler b kriegt auch was player.payoff = C.EMBEZZLE_BUDGET_B group.donation_amount -= C.EMBEZZLE_GROUP_BUDGET #calculate total donation: maximum donation possible minus how many times of embezzlement player.session.total_donation = (len(player.subsession.get_groups()) * C.DONATION_BUDGET) - (player.subsession.amount_embezzling_succeeds * C.EMBEZZLE_GROUP_BUDGET) #store in group_donation in participant variable player.participant.vars['player_team_donation'] = group.donation_amount def group_by_arrival_time_method(subsession, waiting_players): from collections import defaultdict # partner finden, und wenn in eine gruppe möglich ist, rausschmeissen players_grouped_by_matched_before = defaultdict(list) for p in waiting_players: print('norm appropriate groupbyarrival', p.participant,p.participant.matched_before) players_grouped_by_matched_before[p.participant.matched_before].append(p) print(players_grouped_by_matched_before) for group in players_grouped_by_matched_before.values(): if len(group) >= 2: return [group[0],group[1]] # PAGES class Donation_Text_A(Page): @staticmethod def vars_for_template(player: Player): if player.role == C.A_ROLE: player.participant.vars['playertype'] = 'A' player.playertype = player.participant.vars['playertype'] @staticmethod def is_displayed(player: Player): return player.role == C.A_ROLE class Donation_Text_B(Page): @staticmethod def vars_for_template(player: Player): if player.role == C.B_ROLE: player.participant.vars['playertype'] = 'B' player.playertype = player.participant.vars['playertype'] @staticmethod def is_displayed(player: Player): return player.role == C.B_ROLE class Donation_Decision_A(Page): form_model = 'player' form_fields = ['donation_a'] @staticmethod def is_displayed(player: Player): if player.role == C.A_ROLE: player.FirstPartA = True ## nur zum speichern return player.role == C.A_ROLE @staticmethod def before_next_page(player: Player, timeout_happened): ## zum zählen nur if player.role == C.A_ROLE: player.subsession.amount_possible_donations +=1 if player.donation_a: player.subsession.amount_real_donations += 1 else: player.subsession.amount_embezzling_tries += 1 class Donation_Decision_B(Page): form_model = 'player' form_fields = ['donation_b_silence'] @staticmethod def is_displayed(player: Player): return player.role == C.B_ROLE class Donation_Payoff_Waitpage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): fill_control_variables_set_payoff(group) def vars_for_template(player: Player): return dict ( body_text = 'Please wait until your group member made a decision.', ) class Intermediate_Result(Page): @staticmethod def vars_for_template(player: Player): return dict( donated = player.donation_a, silenced = player.donation_b_silence, ##für jeden player donation_a und donation_b_silence gespeichert, daher einfach übergeben myroleA = player.role==C.A_ROLE, #mycorrect = player.estimate_correct kommunizieren wir erst ganz am Ende des Experiments retaliation = C.LOW_RETALIATION if player.subsession.TREATMENT == C.TREATMENTS_NAMES[0] else C.HIGH_RETALIATION ) class Results(Page): @staticmethod def is_displayed(player): return player.round == C.NUM_ROUNDS @staticmethod def vars_for_template(player): return dict( result=5 ### Hier dann rechnen was du brauchst ) class Player_wait_stage(WaitPage): group_by_arrival_time = True #https://otree.readthedocs.io/en/latest/multiplayer/waitpages.html @staticmethod def is_displayed(player): return player.round_number == 1 def vars_for_template(player: Player): return dict (body_text= "Warten Sie bis ihr Teampartner verfügbar ist.") class Puzzle(ExtraModel): player = models.Link(Player) iteration = models.IntegerField(initial=0) attempts = models.IntegerField(initial=0) timestamp = models.FloatField(initial=0) text = models.LongStringField() # entw. Text oder Json (wie in Slider) solution = models.LongStringField() # genauso wie text response = models.LongStringField() response_timestamp = models.FloatField() is_correct = models.BooleanField() def gen_puzzle(player: Player) -> Puzzle: """ generate new puzzle for a player""" ## ignored_chars = player.session.config[ 'ignored_chars'] if 'ignored_chars' in player.session.config else C.IGNORED_CHARS counted_char = player.session.config['counted_char'] if 'counted_char' in player.session.config else C.COUNTED_CHARS width = player.session.config['width'] if 'width' in player.session.config else C.WIDTH height = player.session.config['height'] if 'height' in player.session.config else C.HEIGHT text_size = player.session.config['text_size'] if 'text_size' in player.session.config else C.TEXT_SIZE amount_of_counted_chars = player.session.config[ 'amount_of_counted_chars'] if 'amount_of_counted_chars' in player.session.config else C.AMOUNT_OF_COUNTED_CHARS fields = task_matrix.generate_puzzle_fields(ignored_chars=ignored_chars, counted_char=counted_char, width=width, height=height, text_size=text_size, amount_of_counted_chars=amount_of_counted_chars, amount_empty_fields=C.AMOUNT_EMPTY_FIELDS ) ## Schriftgröße kann hier mitgegeben werden player.iteration += 1 return Puzzle.create( player=player, iteration=player.iteration, timestamp=time.time(), **fields # text & solution ) def get_cur_puzzle(player: Player): puzzles = Puzzle.filter(player=player, iteration=player.iteration) if puzzles: [puzzle] = puzzles return puzzle def enc_puzzle(puzzle: Puzzle): image = task_matrix.render_image(puzzle) return dict(image=encode_image(image)) def get_progess(player: Player): return dict( num_trials=player.num_trials, num_correct=player.num_correct, num_incorrect=player.num_failed, iteration=player.iteration ) def play_game(player: Player, msg: dict): ## siehe slider session = player.session my_id = player.id_in_group params = session.params now = time.time() current = get_cur_puzzle(player) msg_type = msg['type'] if msg_type == 'load': prog = get_progess(player) if current: return { my_id: dict(type='status', progress=prog, puzzle=enc_puzzle(current)) } else: return {my_id: dict(type='status', progress=prog)} if msg_type == 'cheat' and settings.DEBUG: return {my_id: dict(type='solution', solution=current.solution)} if msg_type == 'next': if current is not None: if current.response is None: raise RuntimeError('trying to skip over unsolved puzzle') if now < current.timestamp + params["puzzle_delay"]: raise RuntimeError('retrying too fast') return {my_id: dict(type='status', progress=get_progess(player), iteration_left=0)} puzz = gen_puzzle(player) prog = get_progess(player) return {my_id: dict(type='puzzle', puzzle=enc_puzzle(puzz), progress=prog)} if msg_type == 'answer': if current is None: raise RuntimeError('trying to answer no puzzle') if current.response is not None: # if current.attempts >= params['attempts_per_puzzle']: # raise RuntimeError('no more attempts allowed') # herausgenommen da jeder ja nur einen Versuch hat # if now < current.response_timestamp + params['retry_delay']: # raise RuntimeError('retrying too fast') # herausgenommen, da die Zahl submittet wird, wenn gewünscht dann retry hoch player.num_trials -= 1 if current.is_correct: player.num_correct -= 1 else: player.num_failed -= 1 answer = msg['answer'] if answer == '' or answer is None: raise ValueError('Answer is empty ') current.response = answer current.is_correct = task_matrix.is_correct(answer, current) player.solution = int(current.solution) current.response_timestamp = now current.attempts += 1 if current.is_correct: player.num_correct += 1 else: player.num_failed += 1 player.num_trials += 1 player.response = int(answer) tries_left = params['attempts_per_puzzle'] - current.attempts prog = get_progess(player) return { my_id: dict( type='feedback', is_correct=current.is_correct, retries_left=tries_left, progress=prog, ) } raise RuntimeError("unrecognized message from client") class Spiel(Page): timeout_seconds = C.TIME_PER_PUZZLE if not settings.DEBUG else 10 live_method = play_game @staticmethod def js_vars(player: Player): return dict(params=player.session.params) @staticmethod def vars_for_template(player: Player): return dict(DEBUG=settings.DEBUG, input_type=task_matrix.INPUT_TYPE, placeholder=task_matrix.INPUT_HINT, together=player.participant.vars['count_numbers_together'] ) @staticmethod def before_next_page(player: Player, timeout_happened): if not timeout_happened and not player.session.params['max_iterations']: raise RuntimeError("malicious page submission") '''if C.PLAYERS_PER_GROUP is not None and C.PLAYERS_PER_GROUP > 1: ### An der Stelle Zusammenrechnen der Gruppenberechnung machen, bzw in der Gruppe speichern current = get_cur_puzzle(player) player.group.solution += int (current.solution) player.group.response += int (current.response) ''' @staticmethod def is_displayed(player: Player): if player.session.second_time_counting: if player.round_number > 1: return False else: if player.participant.count_numbers_together == False and player.participant.spieler == 'Spieler2': return False else: return True else: return True # return not (player.session.second_time_counting == True and player.round_number > 1) page_sequence = [Player_wait_stage, Donation_Text_A, Donation_Text_B, Donation_Decision_A, Donation_Decision_B, Donation_Payoff_Waitpage,Intermediate_Result, Results] ### TODO: Moritz 7er Task einfassen in diesen Ablauf ###{7erTask] : [ShuffleWaitPage, Intro, Spiel, Group_wait_stage, Results, BeforeNextApp] #page_sequence = [Player_wait_stage, [7erTask] Donation_Text_A, Donation_Text_B, Donation_Decision_A, Donation_Decision_B, Donation_Payoff_Waitpage,Intermediate_Result, Results] ##TODO: ###Code von App count_numbers_group hier in der init.py einbetten, damit 7er Task direkt aus der init.py aufgerufen ## werden kann. Die Seite soll 7erTask heißen