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