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,
]