import math import random from pathlib import Path from otree.api import * from utils import csv_utils, image_utils doc = """ Stereotypical gender tasks """ WORKDIR = Path(__file__).parent class C(BaseConstants): NAME_IN_URL = "sgt" PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TEXT_FONT = WORKDIR / "assets" / "FreeSansBold.otf" TEXT_SIZE = 64 TEXT_PADDING = 16 TEXT_COLOR = "#000000FF" TEXT_BACKGROUND = "#FFFFFF00" PRACTICE_TIME = 30 COMPETITION_TIME = 60 POST_TRIAL_PAUSE = 1 # pause after answer given / feedback shown # max number of start to use RANK_MAX = 3 TASKS = ["SFT", "SMT"] WORDSPOOL = [] csv_utils.load_csv(WORDSPOOL, WORKDIR / "data" / "words.csv", ["word", "length"]) WORDSPOOL = csv_utils.filter_by_fields(WORDSPOOL, length="5") class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # conditions cond_disclosure = models.StringField() cond_task_type = models.StringField() cond_scored = models.BooleanField() task_name = models.StringField() perf_factor = models.IntegerField() opponent_options = models.StringField() # value is encoded by a function below opponent_choice = models.StringField() opponent_gender = models.StringField() opponent_score = models.IntegerField() practice_time = models.IntegerField(initial=C.PRACTICE_TIME) practice_solved = models.IntegerField(initial=0) practice_score = models.IntegerField() competition_time = models.IntegerField(initial=C.COMPETITION_TIME) competition_solved = models.IntegerField(initial=0) competition_score = models.IntegerField() def encode_options(options): """Converts options from python into single string From [{gender: 'M', score: 1}, {gender: 'F', score: 2}] -> "M1,F2" """ return ",".join([f"{opt['gender']}{opt['score']}" for opt in options]) def decode_options(options_str): """Converts string options field back to python values""" return [{"gender": opt[0], "score": int(opt[1])} for opt in options_str.split(",")] def creating_session(subsession: Subsession): session = subsession.session for player in subsession.get_players(): # get either session-preconfigured or random conditions player.cond_disclosure = session.config.get("cond_disclosure", random.choice(["public", "private"])) player.cond_task_type = session.config.get("cond_task_type", random.choice(C.TASKS)) player.cond_scored = session.config.get("cond_scored", random.choice([True, False])) options = [ {"gender": "M", "score": random.randint(1, C.RANK_MAX)}, {"gender": "F", "score": random.randint(1, C.RANK_MAX)}, ] random.shuffle(options) player.opponent_options = encode_options(options) taskcls = get_task_cls(player) player.task_name = taskcls.__name__ player.perf_factor = taskcls.best_performance_time # Models for specific tasks # hold task-specific parameters and methods class ScrabbleTask(ExtraModel): instructions_html = "scrabble_instructions.html" best_performance_time = 5 # seconds per answer player = models.Link(Player) practice = models.BooleanField() iteration = models.IntegerField() word = models.StringField() letters = models.StringField() answer = models.StringField() is_successful = models.BooleanField() @staticmethod def generate(player: Player, practice): """Generates new scrabble task for player""" word = random.choice(WORDSPOOL)["word"] letters = list(word) random.shuffle(letters) shuffled = "".join(letters) return ScrabbleTask.create( player=player, practice=practice, iteration=player.participant.iteration, # word=word, letters=shuffled, ) def encode(task): """Returns plain data to send to page""" image = image_utils.render_text(C, task.letters) imagedata = image_utils.encode_image(image) return dict(question=task.letters, image=imagedata) def validate(task, answer): """Validates player's response""" task.answer = answer task.is_successful = answer == task.word class MathTask(ExtraModel): instructions_html = "math_instructions.html" best_performance_time = 5 # seconds per answer player = models.Link(Player) practice = models.BooleanField() iteration = models.IntegerField() expression = models.StringField() solution = models.StringField() answer = models.StringField() is_successful = models.BooleanField() @staticmethod def generate(player: Player, practice): """Generates new math task for player""" numbers = [random.randint(11, 99) for i in range(3)] result = sum(numbers) expr = " + ".join(map(str, numbers)) return MathTask.create( player=player, practice=practice, iteration=player.participant.iteration, # expression=expr, solution=result, ) def encode(task): """Returns plain data to send to page""" image = image_utils.render_text(C, task.expression) imagedata = image_utils.encode_image(image) return dict(question=task.expression, image=imagedata) def validate(task, answer): task.answer = answer task.is_successful = answer == task.solution # map of player.cond_task_type to actual models TASK_CLASSES = {"SFT": ScrabbleTask, "SMT": MathTask} # generic utils def get_task_cls(player): return TASK_CLASSES[player.cond_task_type] def get_task(taskcls, pagecls, player): """Get instance of a task trial""" trials = taskcls.filter(player=player, practice=pagecls.practice, iteration=player.participant.iteration) if len(trials) == 0: return None if len(trials) > 1: raise RuntimeError("trials messed up") return trials[0] def get_performance(taskcls, pagecls): """Performance metrics""" return dict( best_time=taskcls.best_performance_time, best_count=math.floor(pagecls.timeout_seconds / taskcls.best_performance_time), ) def vars_for_task(taskcls, pagecls, player): return dict(instructions_html=taskcls.instructions_html, best_performance_time=taskcls.best_performance_time) def score_player(taskcls, pagecls, player): """Calculates score for player Compares actual performance with best performance """ perf = get_performance(taskcls, pagecls) num_solved = len(taskcls.filter(player=player, practice=pagecls.practice, is_successful=True)) return math.ceil(C.RANK_MAX * (num_solved / perf["best_count"])) # PAGES def common_live_method(player, message): """Generic live method for all pages and tasks""" pagecls = page_sequence[player.participant._index_in_pages - 1] taskcls = get_task_cls(player) perf = get_performance(taskcls, pagecls) assert isinstance(message, dict) and "type" in message msgtype = message["type"] print("message received:", message) def send(*messages): print("messages sent:", messages) return {player.id_in_group: messages} if msgtype == "load": """Retrieveing next task or terminating with gameover if maximum trials solved""" if pagecls.practice: solved_count = player.practice_solved else: solved_count = player.competition_solved progressmsg = dict(type="progress", max=perf["best_count"], solved=solved_count) if solved_count == perf["best_count"]: # force game over statusmsg = dict(gameOver=True) return send(statusmsg, progressmsg) player.participant.iteration += 1 task = taskcls.generate(player, pagecls.practice) assert task is not None, "Something failed" print("newtask", task) data = task.encode() trialmsg = dict(type="trial") trialmsg.update(data) return send(trialmsg, progressmsg) if msgtype == "response": """Process response from player""" task = get_task(taskcls, pagecls, player) assert task is not None, "Something failed" task.validate(message["answer"]) if pagecls.practice: if task.is_successful: player.practice_solved += 1 solved_count = player.practice_solved else: if task.is_successful: player.competition_solved += 1 solved_count = player.competition_solved feedbackmsg = dict(type="feedback", responseCorrect=task.is_successful) statusmsg = dict(type="status", trialCompleted=True) progressmsg = dict(type="progress", max=perf["best_count"], solved=solved_count) return send(feedbackmsg, statusmsg, progressmsg) raise RuntimeError("invalid live message type") class Intro(Page): @staticmethod def vars_for_template(player: Player): return vars_for_task(get_task_cls(player), Intro, player) class Score(Page): pass class Choice(Page): form_model = "player" form_fields = ["opponent_choice"] @staticmethod def vars_for_template(player): return dict(options=decode_options(player.opponent_options)) @staticmethod def before_next_page(player: Player, timeout_happened): # split user choice into gender and score gender, score = tuple(player.opponent_choice) player.opponent_gender = gender player.opponent_score = int(score) class Practice(Page): practice = True template_name = "sgt/TaskPage.html" timeout_seconds = C.PRACTICE_TIME @staticmethod def vars_for_template(player: Player): player.participant.iteration = 0 return vars_for_task(get_task_cls(player), Practice, player) @staticmethod def js_vars(player): return dict(post_trial_pause=C.POST_TRIAL_PAUSE, media_fields={"image": "image"}) live_method = common_live_method @staticmethod def before_next_page(player: Player, timeout_happened): player.practice_score = score_player(get_task_cls(player), Practice, player) class Competition(Page): practice = False template_name = "sgt/TaskPage.html" timeout_seconds = C.COMPETITION_TIME @staticmethod def vars_for_template(player: Player): player.participant.iteration = 0 return vars_for_task(get_task_cls(player), Competition, player) @staticmethod def js_vars(player): return dict(post_trial_pause=C.POST_TRIAL_PAUSE, media_fields={"image": "image"}) live_method = common_live_method @staticmethod def before_next_page(player: Player, timeout_happened): player.competition_score = score_player(get_task_cls(player), Competition, player) class Results(Page): pass class Debrief(Page): @staticmethod def vars_for_template(player): player.participant.finished = True page_sequence = [Intro, Practice, Score, Choice, Competition, Results, Debrief]