import time from otree.api import * from intro import ( C as CIntro, CustomTimeoutPage, is_debug, third_party_estimate_label, third_party_popover_body, treatment_has_ai, treatment_is_single, ) from examples import app_after_this_page_for_dropout from markupsafe import Markup doc = """ Your app description """ class C(CIntro): NAME_IN_URL = 'task_group' PLAYERS_PER_GROUP = None NUM_ROUNDS = 6 # Default timeout for all pages, 90 DEFAULT_PAGE_TIMEOUT_SECONDS = 900 # When will the warning be shown, 30 DEFAULT_WARNING_SECONDS = 30 class Subsession(BaseSubsession): actual_score = models.IntegerField() ai_prediction = models.IntegerField() class Group(BaseGroup): avg_prediction = models.IntegerField(min=1, max=100) error = models.IntegerField() # Using bits to represent which players are in the chat # 00: none, 01: player 1, 10: player 2, 11: both player_in_chat = models.IntegerField(min=0, max=3, initial=0) chat_start_time = models.FloatField(default=0) class Player(BasePlayer): private_prediction = models.IntegerField( label=Markup( "What do you think this student's score was? (Please enter a number: 1-100)" ), min=1, max=100, blank=True, null=True, ) shared_prediction = models.IntegerField( label=Markup("Share your estimated score with your partner. You can keep or change your previous estimate.
Note:This estimate will be shared with your partner"), min=1, max=100, blank=True, null=True, ) team_prediction = models.IntegerField( label="What is your final estimate at this stage? (Please enter a number: 1-100)", min=1, max=100, blank=True, null=True, ) # Whether the player has started the chat does_chat = models.BooleanField(initial=False) @property def chat_timeout_in_seconds(self): # return 20 # if self.round_number == 1: # return 90 return 60 # FUNCTIONS def participant_is_single(player: Player) -> bool: if player.participant.vars.get('is_single'): return True return treatment_is_single(player.participant.treatment or '') def finalize_group_prediction(group: Group): players = group.get_players() for pl in players: if pl.team_prediction is None: return data = get_current_round_data(players[0]) avg = sum(p.team_prediction for p in players) / len(players) group.avg_prediction = int(avg + 0.5) group.error = abs(group.avg_prediction - int(data['x2txmthPCT'])) def creating_session(subsession: Subsession): data = subsession.session.vars['data'][subsession.round_number] subsession.actual_score = int(data['x2txmthPCT']) subsession.ai_prediction = int(data['roundFV3']) cfg_treatment = subsession.session.config.get('treatment') if cfg_treatment: t = str(cfg_treatment) for p in subsession.get_players(): p.participant.treatment = t p.participant.vars['treatment'] = t p.participant.vars['is_single'] = treatment_is_single(t) def get_current_round_data(player: Player): return player.session.vars['data'][player.round_number] def task_summary_rows(player: Player): rows = [] for r in range(1, C.NUM_ROUNDS + 1): p = player.in_round(r) rows.append( dict( round_num=r, actual=p.subsession.actual_score, team_estimate=p.group.avg_prediction, points_off=p.group.error, ) ) return rows def vars_for_template(player: Player): t = player.participant.treatment or "" return dict( data=get_current_round_data(player), third_party_estimate_label=third_party_estimate_label(t), third_party_popover_body=third_party_popover_body(t), ) def private_vars_for_template(player: Player): out = vars_for_template(player) out['is_single'] = participant_is_single(player) return out def private_before_next_page(player: Player, timeout_happened: bool): set_dropout(player, timeout_happened) player.shared_prediction = player.private_prediction def app_after_this_page_for_teammate_dropout(player: Player, upcoming_apps): participant = player.participant if participant.is_dropout or participant.is_teammate_dropout or participant.waiting_too_long: return 'dropout' def waiting_too_long(player: Player): participant = player.participant participant.vars['waiting_too_long'] = time.time() - participant.vars['wait_page_arrival_time'] > C.WAITING_PAGE_THRESHOLD_SECONDS return participant.vars['waiting_too_long'] # Mix of size-1 groups (single treatments) and pairs matched on treatment def group_by_arrival_time_method(subsession: Subsession, waiting_players: list[Player]): for p in waiting_players: if p.participant.is_dropout or waiting_too_long(p): return [p] for p in waiting_players: if participant_is_single(p): return [p] for treatment in C.TREATMENTS: treatment_players = [ p for p in waiting_players if p.participant.treatment == treatment and not participant_is_single(p) ] if len(treatment_players) >= 2: return treatment_players[:2] return None def get_teammate_participant(player: Player): teammate = player.get_others_in_group() # Only one teammate is true teammate if len(teammate) != 1: return None return teammate[0].participant def set_dropout(player: Player, timeout_happened: bool): # When debug, this will enable advance. if timeout_happened and not is_debug(player): player.participant.vars['is_dropout'] = True teammate = get_teammate_participant(player) if teammate is not None: teammate.vars['is_teammate_dropout'] = True def live_method_for_warning_count(player: Player, data): if isinstance(data, dict) and data.get('type') == 'warning_count_increase': player.participant.vars['warning_count'] += 1 # Live method for attention check def live_method_for_attention_check(player: Player, data): print(data) live_method_for_warning_count(player, data) # Make it same as javascript timestamp if data.get('attention_check') == 'confirmed': player.participant.vars['last_check_time'] = int(time.time() * 1000) elif data.get('attention_check') == 'timeout': player.participant.is_dropout = True teammate = get_teammate_participant(player) if teammate is not None: teammate.is_teammate_dropout = True print(player.participant.is_dropout) return {player.id_in_group: data} # PAGES # Wait page before the whole task, aim to group players by treatment class GroupWait(WaitPage): title_text = "Actual Prediction" group_by_arrival_time = True live_method = live_method_for_attention_check app_after_this_page = app_after_this_page_for_teammate_dropout template_name = 'task_group/includes/GroupWaitPage.html' @staticmethod def is_displayed(player: Player): return player.round_number == 1 # Wait page before next round class Wait1(WaitPage): app_after_this_page = app_after_this_page_for_teammate_dropout before_next_page = set_dropout live_method = live_method_for_attention_check template_name = 'task_group/includes/WaitPageTemplate.html' pass # Wait page before chat class Wait2(Wait1): pass class Private(CustomTimeoutPage): form_model = 'player' form_fields = ['private_prediction'] app_after_this_page = app_after_this_page_for_teammate_dropout before_next_page = private_before_next_page live_method = live_method_for_attention_check vars_for_template = staticmethod(private_vars_for_template) @staticmethod def error_message(player: Player, values): if values['private_prediction'] is None: return 'Please enter your predicted score' class Chat(CustomTimeoutPage): # Pairs: discussion can cause dropout if chat never starts (see before_next_page). # Singles: info-only; no chat dropout. Final prediction is submitted on Team. # Timeout for the chat page if the discussion never starts timeout_seconds_before_discussion = C.DEFAULT_PAGE_TIMEOUT_SECONDS # Timeout for the chat page if the discussion ends normally timeout_seconds_after_discussion = C.DEFAULT_PAGE_TIMEOUT_SECONDS timeout_seconds = timeout_seconds_before_discussion warning_seconds = C.DEFAULT_WARNING_SECONDS app_after_this_page = app_after_this_page_for_teammate_dropout @staticmethod def vars_for_template(player: Player): t = player.participant.treatment or '' out = dict( contains_ai=treatment_has_ai(t), data=get_current_round_data(player), my_prediction=player.field_maybe_none('shared_prediction'), third_party_estimate_label=third_party_estimate_label(t), third_party_popover_body=third_party_popover_body(t), is_single=participant_is_single(player), ) if participant_is_single(player): return out other_player = player.get_others_in_group()[0] out['teammate_prediction'] = other_player.field_maybe_none('shared_prediction') return out @staticmethod def js_vars(player: Player): return dict( round_number=player.round_number, player_id=player.id_in_group, chat_timeout_in_seconds=player.chat_timeout_in_seconds, warning_seconds=Chat.warning_seconds, timeout_seconds=Chat.timeout_seconds, ) @staticmethod def live_method(player: Player, data): if participant_is_single(player): return {player.id_in_group: data} group = player.group current_time = time.time() * 1000 # Handle time sync request if isinstance(data, dict) and data.get('type') == 'sync_time': return { player.id_in_group: { 'sync_response': True, 'server_time': current_time, 'client_time': data.get('client_time') } } # Handle timer check request if isinstance(data, dict) and data.get('type') == 'check_timer': if group.chat_start_time > 0: return {0: { 'start_timer': True, 'start': group.chat_start_time, 'current_time': current_time }} return {0: {'start_timer': False}} # Handle chat message if isinstance(data, dict) and data.get('type') == 'chat_message': bitmask = 1 << (player.id_in_group - 1) group.player_in_chat = group.player_in_chat | bitmask if not player.does_chat: player.does_chat = True # print("Player {} starts chat".format(player.id_in_group)) if group.player_in_chat == 3: if not group.chat_start_time: group.chat_start_time = current_time return {0: { 'start_timer': True, 'start': group.chat_start_time, 'current_time': current_time }} return {0: {'start_timer': False}} @staticmethod def before_next_page(player: Player, timeout_happened): if participant_is_single(player): return if timeout_happened and not is_debug(player): if not player.does_chat: player.participant.vars['is_dropout'] = True else: teammate = player.get_others_in_group()[0] if teammate is not None: if not teammate.does_chat: player.participant.vars['is_teammate_dropout'] = True class Team(CustomTimeoutPage): form_model = 'player' form_fields = ['team_prediction'] live_method = live_method_for_attention_check app_after_this_page = app_after_this_page_for_teammate_dropout @staticmethod def before_next_page(player: Player, timeout_happened): set_dropout(player, timeout_happened) if participant_is_single(player): finalize_group_prediction(player.group) @staticmethod def vars_for_template(player: Player): t = player.participant.treatment or '' others = player.get_others_in_group() other_player = others[0] if len(others) == 1 else None teammate_pred = ( other_player.field_maybe_none('shared_prediction') if other_player else None ) return dict( contains_ai=treatment_has_ai(t), data=get_current_round_data(player), my_prediction=player.field_maybe_none('shared_prediction'), teammate_prediction=teammate_pred, third_party_estimate_label=third_party_estimate_label(t), third_party_popover_body=third_party_popover_body(t), is_single=participant_is_single(player), ) @staticmethod def error_message(player: Player, values): if values['team_prediction'] is None: return 'Please enter your final predicted score' class Wait3(WaitPage): app_after_this_page = app_after_this_page_for_teammate_dropout before_next_page = set_dropout live_method = live_method_for_attention_check template_name = 'task_group/includes/WaitPageTemplate.html' @staticmethod def is_displayed(player: Player): return not participant_is_single(player) @staticmethod def after_all_players_arrive(group: Group): finalize_group_prediction(group) class TaskSummary(CustomTimeoutPage): live_method = live_method_for_attention_check app_after_this_page = app_after_this_page_for_dropout @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): rows = task_summary_rows(player) total_points_off = sum(row['points_off'] for row in rows) return dict(summary_rows=rows, summary_total_points_off=total_points_off, is_single=participant_is_single(player)) @staticmethod def before_next_page(player: Player, timeout_happened): set_dropout(player, timeout_happened) predicted_scores = sum([p.team_prediction for p in player.in_all_rounds()]) errors_in_rounds = [p.group.error for p in player.in_all_rounds()] total_error = sum(errors_in_rounds) player.participant.vars['task_total_error'] = total_error player.participant.vars['task_avg_error'] = total_error / C.NUM_ROUNDS player.participant.vars['task_predicted_scores'] = predicted_scores for i in range(C.NUM_ROUNDS): player.participant.vars['task_error_in_round' + str(i + 1)] = errors_in_rounds[i] player.participant.vars['task_payoff'] = C.BONUSES[4] for i in range(len(C.BONUS_THRESHOLD)): if total_error <= C.BONUS_THRESHOLD[i]: player.participant.vars['task_payoff'] = C.BONUSES[i] break player.participant.payoff = player.participant.task_payoff page_sequence = [ GroupWait, Wait1, Private, Wait2, Chat, Team, Wait3, TaskSummary, ]