import time from otree import settings from otree.api import * from .image_utils import encode_image doc = """ Letter-to-number encryption task. """ #CLASSES ##SESSION def get_task_module(player): """ This function is only needed for demo mode, to demonstrate all the different versions. You can simplify it if you want. """ from . import task_matrix, task_decoding session = player.session task = session.config.get("task") if task == "matrix": return task_matrix if task == "decoding": return task_decoding # default return task_matrix class Constants(BaseConstants): name_in_url = "Transcription" players_per_group = None num_rounds = 1 TREATMENTS = [0, 1] instructions_template = __name__ + "/instructions_short.html" captcha_length = 3 ##SUBSESSION class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session session.participants_per_treatments = {piecerate: 0 for piecerate in Constants.TREATMENTS} ##GROUP class Group(BaseGroup): pass ##PLAYER class Player(BasePlayer): comm_low1 = models.IntegerField( initial=0, min=5, max=50, choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], label="Medium payment", ) comm_high2 = models.IntegerField( initial=0, min=5, max=50, choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], label="High payment", ) piecerate = models.IntegerField( choices=[[0, 'Low'], [1, 'High']], ) piecerate_cents = models.IntegerField( choices=[[0, '5 cents (~0.06 USD)'], [1, '50 cents (~0.60 USD)']], ) commitment = models.IntegerField() redo = models.BooleanField(initial=False) iteration = models.IntegerField(initial=0) num_trials = models.IntegerField(initial=0) num_correct = models.IntegerField(initial=0) num_failed = models.IntegerField(initial=0) bonus_payment = models.FloatField(initial=0) time0 = models.FloatField() time1 = models.FloatField() time2 = models.FloatField() time3 = models.FloatField() time4 = models.FloatField() time5 = models.FloatField() time6 = models.FloatField() time7 = models.FloatField() time8 = models.FloatField() time9 = models.FloatField() time10 = models.FloatField() time11 = models.FloatField() time12 = models.FloatField() time13 = models.FloatField() time14 = models.FloatField() time15 = models.FloatField() time16 = models.FloatField() time17 = models.FloatField() time18 = models.FloatField() time19 = models.FloatField() time20 = models.FloatField() time21 = models.FloatField() time22 = models.FloatField() time23 = models.FloatField() time24 = models.FloatField() time25 = models.FloatField() time26 = models.FloatField() time27 = models.FloatField() time28 = models.FloatField() time29 = models.FloatField() time30 = models.FloatField() time31 = models.FloatField() time32 = models.FloatField() time33 = models.FloatField() time34 = models.FloatField() time35 = models.FloatField() time36 = models.FloatField() time37 = models.FloatField() time38 = models.FloatField() time39 = models.FloatField() time40 = models.FloatField() time41 = models.FloatField() time42 = models.FloatField() time43 = models.FloatField() time44 = models.FloatField() time45 = models.FloatField() time46 = models.FloatField() time47 = models.FloatField() time48 = models.FloatField() time49 = models.FloatField() time50 = models.FloatField() def select_piecerate(player: Player): session = player.session # lottery player.piecerate = min(Constants.TREATMENTS, key=lambda piecerate: session.participants_per_treatments[piecerate]) session.participants_per_treatments[player.piecerate] += 1 ## auxiliars submission = [player.comm_low1, player.comm_high2] player.commitment = submission[player.piecerate] player.piecerate_cents = player.piecerate def piecerate_value(independent): if independent == 0: return 0.05 elif independent == 1: return 0.50 def set_payoff(player: Player): if player.num_correct >= 5: if player.num_correct == player.commitment: rate=piecerate_value(player.piecerate) player.bonus_payment = rate * player.num_correct ##PUZZLE class Puzzle(ExtraModel): """A model to keep record of all generated puzzles""" player = models.Link(Player) iteration = models.IntegerField(initial=0) attempts = models.IntegerField(initial=0) timestamp = models.FloatField(initial=0) # can be either simple text, or a json-encoded definition of the puzzle, etc. text = models.LongStringField() # solution may be the same as text, if it's simply a transcription task solution = models.LongStringField() response = models.LongStringField() response_timestamp = models.FloatField() is_correct = models.BooleanField() def calculate_dependent(independent): if independent >= 0 and independent < 5: return 1 elif independent >= 5 and independent < 10: return 2 elif independent >= 10 and independent < 15: return 3 elif independent >= 15 and independent < 20: return 4 elif independent >= 20 and independent < 25: return 5 elif independent >= 25 and independent < 30: return 6 elif independent >= 30 and independent < 35: return 7 elif independent >= 35 and independent < 40: return 8 elif independent >= 40 and independent < 45: return 9 elif independent >= 45 and independent < 50: return 10 else: return 15 # Return -1 for out-of-range values or handle it differently based on your requirements def generate_puzzle(player: Player) -> Puzzle: """Create new puzzle for a player""" task_module = get_task_module(player) WORD_LENGTH=calculate_dependent(player.iteration) fields = task_module.generate_puzzle_fields(WORD_LENGTH) player.iteration += 1 return Puzzle.create( player=player, iteration=player.iteration, timestamp=time.time(), **fields ) def get_current_puzzle(player): puzzles = Puzzle.filter(player=player, iteration=player.iteration) if puzzles: [puzzle] = puzzles return puzzle def encode_puzzle(puzzle: Puzzle): """Create data describing puzzle to send to client""" task_module = get_task_module(puzzle.player) # noqa # generate image for the puzzle image = task_module.render_image(puzzle) data = encode_image(image) return dict(image=data) def get_progress(player: Player): """Return current player progress""" 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, message: dict): """Main game workflow Implemented as reactive scheme: receive message from vrowser, react, respond. Generic game workflow, from server point of view: - 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': 'next'} -- request for a next/first puzzle - generate new puzzle - respond: {'type': 'puzzle', 'puzzle': data} - receive: {'type': 'answer', 'answer': ...} -- user answered the puzzle - check if the answer is correct - respond: {'type': 'feedback', 'is_correct': true|false, 'retries_left': ...} -- feedback to the answer If allowed by config `attempts_pre_puzzle`, client can send more 'answer' messages When done solving, client should explicitely request next puzzle by sending 'next' message Field 'progress' is added to all server responses to indicate it on page. To indicate max_iteration exhausted in response to 'next' server returns 'status' message with iterations_left=0 """ session = player.session my_id = player.id_in_group total_games = player.commitment params = session.params task_module = get_task_module(player) now = time.time() current = get_current_puzzle(player) message_type = message['type'] max_correct = session.config['max_correct'] # page loaded if message_type == 'load': p = get_progress(player) if player.num_correct == 0: player.time0 = round(now,3) if current: return { my_id: dict(type='status', progress=p, puzzle=encode_puzzle(current)) } else: return {my_id: dict(type='status', progress=p)} if message_type == "cheat" and settings.DEBUG: return {my_id: dict(type='solution', solution=current.solution)} # client requested new puzzle if message_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") if player.num_correct == total_games: return { my_id: dict( type='status', progress=get_progress(player), iterations_left=0 ) } # generate new puzzle z = generate_puzzle(player) p = get_progress(player) return {my_id: dict(type='puzzle', puzzle=encode_puzzle(z), progress=p)} # client gives an answer to current puzzle if message_type == "answer": if current is None: raise RuntimeError("trying to answer no puzzle") if current.response is not None: # it's a retry if now < current.response_timestamp + params["retry_delay"]: raise RuntimeError("retrying too fast") # check answer answer = message["answer"] if answer == "" or answer is None: raise ValueError("bogus answer") current.response = answer current.is_correct = task_module.is_correct(answer, current) current.response_timestamp = now current.attempts += 1 # update player progress if current.is_correct: player.num_correct += 1 task = player.num_correct if task == 1: player.time1 = round(now,3) elif task == 2: player.time2 = round(now,3) elif task == 3: player.time3 = round(now,3) elif task == 4: player.time4 = round(now,3) elif task == 5: player.time5 = round(now,3) elif task == 6: player.time6 = round(now,3) elif task == 7: player.time7 = round(now,3) elif task == 8: player.time8 = round(now,3) elif task == 9: player.time9 = round(now,3) elif task == 10: player.time10 = round(now,3) elif task == 11: player.time11 = round(now,3) elif task == 12: player.time12 = round(now,3) elif task == 13: player.time13 = round(now,3) elif task == 14: player.time14 = round(now,3) elif task == 15: player.time15 = round(now,3) elif task == 16: player.time16 = round(now,3) elif task == 17: player.time17 = round(now,3) elif task == 18: player.time18 = round(now,3) elif task == 19: player.time19 = round(now,3) elif task == 20: player.time20 = round(now,3) elif task == 21: player.time21 = round(now,3) elif task == 22: player.time22 = round(now,3) elif task == 23: player.time23 = round(now,3) elif task == 24: player.time24 = round(now,3) elif task == 25: player.time25 = round(now,3) elif task == 26: player.time26 = round(now,3) elif task == 27: player.time27 = round(now,3) elif task == 28: player.time28 = round(now,3) elif task == 29: player.time29 = round(now,3) elif task == 30: player.time30 = round(now,3) elif task == 31: player.time31 = round(now,3) elif task == 32: player.time32 = round(now,3) elif task == 33: player.time33 = round(now,3) elif task == 34: player.time34 = round(now,3) elif task == 35: player.time35 = round(now,3) elif task == 36: player.time36 = round(now,3) elif task == 37: player.time37 = round(now,3) elif task == 38: player.time38 = round(now,3) elif task == 39: player.time39 = round(now,3) elif task == 40: player.time40 = round(now,3) elif task == 41: player.time41 = round(now,3) elif task == 42: player.time42 = round(now,3) elif task == 43: player.time43 = round(now,3) elif task == 44: player.time44 = round(now,3) elif task == 45: player.time45 = round(now,3) elif task == 46: player.time46 = round(now,3) elif task == 47: player.time47 = round(now,3) elif task == 48: player.time48 = round(now,3) elif task == 49: player.time49 = round(now,3) elif task == 50: player.time50 = round(now,3) else: player.num_failed += 1 player.num_trials += 1 retries_left = 1 p = get_progress(player) return { my_id: dict( type='feedback', is_correct=current.is_correct, retries_left=retries_left, progress=p, ) } raise RuntimeError("unrecognized message from client") #PAGES class Screen1(Page): pass class Screen2(Page): pass class Instructions(Page): pass class Trial(Page): pass class Instructions(Page): pass class Commitment(Page): form_model = 'player' form_fields = ['comm_low1', 'comm_high2'] timer_text = 'Time left to continue:' @staticmethod def error_message(player: Player, values): for item in values: if values[item] < 5: return "One or more encryption commitment is below 5. You need to complete at least 5 encryptions." @staticmethod def vars_for_template(player: Player): return dict(DEBUG=settings.DEBUG) class CommCheck(Page): form_model = 'player' form_fields = ['redo'] @staticmethod def vars_for_template(player): return dict( comm1=player.comm_low1, comm2=player.comm_high2, ) class Commitment2(Page): form_model = 'player' form_fields = ['comm_low1','comm_high2'] @staticmethod def error_message(player: Player, values): for item in values: if values[item] < 5: return "One or more encryption commitment is below 5. You need to complete at least 5 encryptions." @staticmethod def js_vars(player): return dict( comm1=player.comm_low1, comm2=player.comm_high2, ) @staticmethod def is_displayed(player): return player.redo == True class CommFinal(Page): timeout_seconds = 5 @staticmethod def vars_for_template(player): return dict( comm1=player.comm_low1, comm2=player.comm_high2, ) @staticmethod def before_next_page(player, timeout_happened): select_piecerate(player) class CommResult(Page): @staticmethod def vars_for_template(player): return dict( piecerate=player.field_display('piecerate'), piecerate_cents=player.field_display('piecerate_cents'), commitment=player.commitment, ) class Game(Page): timeout_seconds = None live_method = play_game @staticmethod def js_vars(player: Player): return dict(params=player.session.params) @staticmethod def vars_for_template(player: Player): task_module = get_task_module(player) return dict(DEBUG=settings.DEBUG, input_type=task_module.INPUT_TYPE) @staticmethod def before_next_page(player, timeout_happened): set_payoff(player) player.participant.finished = True class Results(Page): @staticmethod def vars_for_template(player): return dict( commitment=player.commitment, num_correct=player.num_correct, ) #SEQUENCE page_sequence = [Instructions, Commitment, CommCheck, Commitment2, CommFinal, CommResult, Game, Results]