import random ### Standard Imports import json import time from otree import settings from otree.api import * ## Imports von unseren eigenen Dateien from . import slider_task ## Slider Verarbeitung from .image_utils import encode_image ## Bild Verarbeitung doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'corruption' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3#10 TREATMENTS = ['real_effort', 'endowment'] ADDITIONAL_PAYOFF_PROB = 0.2#8/10 ## 80% MULTIPLICATOR = 10 ## nimm den nenner von oben ADDITIONAL_PAYOFF_ALTERNATIVES = __name__ + '/alternatives_additional_payment.html' ADDITIONAL_PAYOFF_ADDITION = 1/4 '''real effort specifics''' RE_SLIDER_PAYOFF = cu(10) RE_BLOCKED_SLIDER_AMOUNT = 10 RE_BLOCKED_SLIDER_PAYOFF = cu(2) RE_TIME_TASK = 120 RE_ADDITIONAL_PAYMENT_PAGE = __name__ + "/realeffort_additional_payment.html" RE_INFO_PAGE = __name__ + "/realeffort_intro.html" '''endowment specifics''' ENDOWMENT_PAYOFF = cu(80) ENDOWMENT_BLOCKED_PAYOFF = cu(20) ENDOWMENT_ADDITIONAL_PAYMENT_PAGE = __name__ + "/endowment_additional_payment.html" ENDOWMENT_INFO_PAGE = __name__ + "/endowment_intro.html" def creating_session(subsession): MSG_TREATMENT_MISSING = """ FAILURE: The Session Config has a not specified Treatment, please check if Constant TREATMENTS has the given config-treatment in its list """ if 'treatment' in subsession.session.config: subsession.Treatment = subsession.session.config['treatment'] if subsession.Treatment not in C.TREATMENTS: raise SystemExit(MSG_TREATMENT_MISSING) defaults = dict( num_sliders=48,#anzahl der Slider num_columns=3, #anzahl der Spalten attempts_per_slider = 5, # anzahl versuche pro Slider (mach sehr groß wenn nicht gebraucht) ## Delays hinzufügen? trial_delay = 1, retry_delay = 1/10, ## Zeit zwischen falscher Eingabe und neu Versuch ) subsession.session.params = {} for param in defaults: subsession.session.params[param] = subsession.session.config.get(param, defaults[param]) class Subsession(BaseSubsession): Treatment = models.StringField(initial=C.TREATMENTS[0]) ##real effort class Group(BaseGroup): pass class Player(BasePlayer): is_blocked = models.BooleanField(initial=False) additional_payment_choice = models.BooleanField( label="", choices=[ [True, 'Ja, Zusatzzahlung wählen'], [False,'Nein, Zusatzzahlung nicht wählen'], ], widget=widgets.RadioSelectHorizontal ) additional_payment_achieved = models.BooleanField(initial=False) iteration = models.IntegerField(initial=0) ## nur dafür getestet num_correct = models.IntegerField(initial=0) ## anzahl richtiger time_elapsed = models.FloatField(initial=0) ## Vergangene Zeit num_false = models.IntegerField(initial=0) ### Extra Klasse für Spezifizierungen class Puzzle(ExtraModel): ## Idee ist für die anderen Grundspiele identisch """Model for Slider Setup""" player= models.Link(Player) iteration = models.IntegerField() timestamp = models.FloatField() response_timestamp = models.FloatField() num_sliders = models.IntegerField() layout=models.LongStringField() num_correct = models.IntegerField(initial=0) is_solved = models.BooleanField(initial=False) class Slider(ExtraModel): """Model pro Slider""" puzzle = models.Link(Puzzle) id_slider = models.IntegerField() target = models.IntegerField() value = models.IntegerField() is_correct = models.BooleanField(initial=False) attempts = models.IntegerField(initial=0) ### Ende Extraklassen ### Zusätzliche Methoden def gen_puzzle(player: Player) -> Puzzle: #kriegt nen Spieler -> gibt nen Puzzle """Create new Puzzle for a Player""" params = player.session.params num_sliders = params['num_sliders'] num_columns = params['num_columns'] if player.is_blocked: num_sliders = C.RE_BLOCKED_SLIDER_AMOUNT num_columns = 1 #change params params['num_sliders'] = num_sliders params['num_columns'] = num_columns layout = slider_task.generate_layout(params) ## kommt aus der extra python die oben importiert wurde puzzle = Puzzle.create( player = player, iteration = player.iteration, timestamp = time.time(), # start Zeit num_sliders = num_sliders, layout = json.dumps(layout) ) for i in range(num_sliders): target, initial = slider_task.generate_slider() Slider.create( puzzle = puzzle, id_slider = i, target = target, value = initial ) return puzzle def get_cur_puzzle(player:Player): puzzles = Puzzle.filter(player=player, iteration=player.iteration) if puzzles: [puzzle] = puzzles return puzzle def get_slider(puzzle: Puzzle, id_slider): sliders = Slider.filter(puzzle=puzzle , id_slider=id_slider) if sliders: [slider] = sliders return slider def enc_puzzle(puzzle:Puzzle): """Sent to Client - description of puzzle""" layout = json.loads(puzzle.layout) sliders = Slider.filter(puzzle=puzzle) ## Bild draus machen image = slider_task.render_image(layout, targets= [s.target for s in sliders]) return dict( image= encode_image(image), size = layout['size'], grid = layout['grid'], sliders = {s.id_slider: {'value':s.value, 'is_correct':s.is_correct} for s in sliders} ) def get_progress(player:Player): """Return Progress of current Player""" return dict( iteration=player.iteration, solved = player.num_correct ) def handle_resp(puzzle,slider,value): slider.value = slider_task.snap_value(value, slider.target) slider.is_correct = slider.value == slider.target ## nur korrekt wenn es genau gleich ist puzzle.num_correct = len(Slider.filter(puzzle=puzzle, is_correct=True)) puzzle.is_solved = puzzle.num_correct == puzzle.num_sliders ### Ende Zusätzliche Methoden ## eigentliche Arbeit: def play_game(player:Player, msg: dict): """ Game workflow: Receive Message from browser, use data , give answer Serverpov for workflow: - receive: {'type': 'load'} -- empty message means page loaded - check if it's game start or page refresh midgame - respond: {'type': 'status', 'progress': ...} - respond: {'type': 'status', 'progress': ..., 'puzzle': data} in case of midgame page reload - receive: {'type': 'new'} -- request for a new puzzle - generate new sliders - respond: {'type': 'puzzle', 'puzzle': data} - receive: {'type': 'value', 'slider': ..., 'value': ...} -- submitted value of a slider - slider: the index of the slider - value: the value of slider in pixels - check if the answer is correct - respond: {'type': 'feedback', 'slider': ..., 'value': ..., 'is_correct': ..., 'is_completed': ...} - slider: the index of slider submitted - value: the value aligned to slider steps - is_corect: if submitted value is correct - is_completed: if all sliders are correct """ session = player.session my_id = player.id_in_group params = session.params now = time.time() puzzle = get_cur_puzzle(player) ## entweder leer(none) oder das aktuelle msg_type = msg['type'] if msg_type == 'load': prog = get_progress(player) id_dict = dict(type='status', progress=prog) if puzzle: ## falls n puzzle vorhanden ist, häng es dran id_dict['puzzle'] = enc_puzzle(puzzle) return {my_id: id_dict} if msg_type == 'new': if puzzle is not None: raise RuntimeError('Trying to create another Puzzle') player.iteration += 1 puzz = gen_puzzle(player) prog = get_progress(player) return {my_id: dict(type='puzzle', puzzle=enc_puzzle(puzz), progress=prog)} if msg_type == 'value': if puzzle is None: raise RuntimeError('Puzzle is missing') ## Spieler versucht es zu schnell wieder (retry delay, kleiner machen falls euch das häufiger passiert) if puzzle.response_timestamp and now < puzzle.response_timestamp + params['retry_delay']: raise RuntimeError('Retrying to fast') slider = get_slider(puzzle, int(msg['slider'])) if slider is None: raise RuntimeError('Slider is missing') if slider.attempts >= params['attempts_per_slider']: raise RuntimeError('Too many slider motions') value = int(msg['value']) handle_resp(puzzle,slider,value) puzzle.response_timestamp = now slider.attempts+=1 player.num_correct = puzzle.num_correct prog = get_progress(player) return { my_id: dict( type = 'feedback', slider=slider.id_slider, value=slider.value, is_correct = slider.is_correct, is_completed = puzzle.is_solved, progress = prog ) } if msg_type == 'cheat' and settings.DEBUG: ## nur für euch damit ihr es nicht machen müsst return {my_id: dict(type='solution', solution={s.id_slider: s.target for s in Slider.filter(puzzle=puzzle)})} #sonst, heißt er kennt den msg_type nicht raise RuntimeError('unrecognized message from Client') # PAGES # PAGES class GetBlockedStatus(WaitPage): @staticmethod def is_displayed(player: Player): if player.round_number > 1: ##Test if thats okay player.is_blocked = player.in_previous_rounds()[-1].is_blocked return False class TestInfo(Page): @staticmethod def is_displayed(player: Player): ## nur in realeffort und ersten runde return player.subsession.Treatment == 'real_effort' and player.round_number == 1 class TestSlider(Page): @staticmethod def is_displayed(player: Player): ## nur in realeffort und ersten runde return player.subsession.Treatment == 'real_effort' and player.round_number == 1 timeout_seconds = C.RE_TIME_TASK live_method = play_game @staticmethod def js_vars(player: Player): return dict( params= player.session.params, slider_size = slider_task.SLIDER_BBOX, ) @staticmethod def vars_for_template(player: Player): return dict( params = player.session.params, DEBUG = settings.DEBUG ) @staticmethod def before_next_page(player: Player, timeout_happened): puzzle = get_cur_puzzle(player) if puzzle and puzzle.response_timestamp: player.time_elapsed = puzzle.response_timestamp - puzzle.timestamp player.num_correct = puzzle.num_correct #kein geld für den Test #money_per_slider = C.RE_BLOCKED_SLIDER_PAYOFF if player.is_blocked else C.RE_SLIDER_PAYOFF #player.payoff = player.num_correct * money_per_slider player.num_false = puzzle.num_sliders - puzzle.num_correct ## Setze neues puzzle ein player.iteration = 0 gen_puzzle(player) class TestResult(Page): @staticmethod def is_displayed(player: Player): ## nur in realeffort und ersten runde return player.subsession.Treatment == 'real_effort' and player.round_number == 1 class InfoPage(Page): @staticmethod def vars_for_template(player: Player): return dict( real_effort= player.subsession.Treatment == 'real_effort' ) class RealEffort(Page): timeout_seconds = C.RE_TIME_TASK live_method = play_game def is_displayed(player: Player): ## nur in realeffort return player.subsession.Treatment == 'real_effort' @staticmethod def js_vars(player: Player): return dict( params=player.session.params, slider_size=slider_task.SLIDER_BBOX, ) @staticmethod def vars_for_template(player: Player): return dict( params=player.session.params, DEBUG=settings.DEBUG ) @staticmethod def before_next_page(player: Player, timeout_happened): puzzle = get_cur_puzzle(player) if puzzle and puzzle.response_timestamp: player.time_elapsed = puzzle.response_timestamp - puzzle.timestamp player.num_correct = puzzle.num_correct money_per_slider = C.RE_BLOCKED_SLIDER_PAYOFF if player.is_blocked else C.RE_SLIDER_PAYOFF player.payoff = player.num_correct * money_per_slider player.num_false = puzzle.num_sliders - puzzle.num_correct class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): for player in group.get_players(): if player.subsession.Treatment == 'endowment': ## hier die Berechnung machen if player.is_blocked: player.payoff = C.ENDOWMENT_BLOCKED_PAYOFF else: player.payoff = C.ENDOWMENT_PAYOFF ## realeffort wird bei den slidern schon gemacht class Results(Page): @staticmethod def vars_for_template(player: Player): return dict( real_effort=C.TREATMENTS == player.subsession.Treatment ,payoff = player.payoff, num_correct = player.num_correct ##muss man nicht war aber schneller für mich hier ) class AdditionalPayment(Page): form_model = 'player' form_fields = ['additional_payment_choice'] @staticmethod def is_displayed(player: Player): return not player.is_blocked @staticmethod def vars_for_template(player: Player): return dict( real_effort=C.TREATMENTS == player.subsession.Treatment, alternative1_amount = int(C.ADDITIONAL_PAYOFF_PROB*C.MULTIPLICATOR), alternative1_extra = C.ADDITIONAL_PAYOFF_ADDITION*100, alternative2_amount = int(C.MULTIPLICATOR - (C.ADDITIONAL_PAYOFF_PROB)*C.MULTIPLICATOR), ## bei 1-prob * multiplicator kam er auf 1,996 und das hat er abgerundet ) class AdditionalPaymentResult(Page): @staticmethod def is_displayed(player: Player): return not player.is_blocked and player.additional_payment_choice @staticmethod def vars_for_template(player: Player): if player.additional_payment_choice: if random.random() < C.ADDITIONAL_PAYOFF_PROB: player.additional_payment_achieved = 1 player.payoff += player.payoff*C.ADDITIONAL_PAYOFF_ADDITION else: player.is_blocked = True return dict( payoff = player.payoff, blocked = player.is_blocked, real_effort=C.TREATMENTS == player.subsession.Treatment, ) class FinalResult(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS #todo: Settings die umrechnung anpassen - Am besten marius mal fragen ob der Umrechnungsfaktor nur global oder pro spiel geht @staticmethod def vars_for_template(player: Player): return dict( payoff = player.participant.payoff, payoff_fee = player.participant.payoff_plus_participation_fee() ) page_sequence = [GetBlockedStatus,TestInfo, TestSlider, TestResult, InfoPage, RealEffort, ResultsWaitPage, Results, AdditionalPayment, AdditionalPaymentResult, FinalResult]