from otree.api import *
doc = """
Your app description
"""
class C(BaseConstants):
NAME_IN_URL = 'mpl_table'
PLAYERS_PER_GROUP = None
NUM_ROUNDS = 9
NUM_BALLS_IN_URN = 90
GROUPS = ['A', 'B', 'C']
TASKS = ['buy', 'sell', 'short_sell']
TASK_DICT = dict(
buy="Buying",
sell="Selling",
short_sell="Issuing"
)
PROSPECTS = ['risk', 'partial', 'uncertainty']
PROSPECTS_DICT = dict(
risk="RISK",
partial="AMBIGUITY",
uncertainty="IGNORANCE"
)
PAYOFF_PAIRS_IDS = ['a', 'c']
PAYOFF_PAIRS = dict(
a=(100, 600),
c=(300, 400)
)
MPL_TABLE_HEADERS = dict(
buy=(
"If the price was...",
"...I certainly
would buy",
"...I am not sure",
"...I certainly
would not buy"
),
sell=(
"If the price was...",
"...I certainly
would sell",
"...I am not sure",
"...I certainly
would not sell"
),
short_sell=(
"If the price was...",
"...I certainly
would issue the ticket",
"...I am not sure",
"...I certainly
would not issue the ticket"
)
)
MPL_FEEDBACK = dict(
buy=(
"you certainly would buy",
"you certainly wouldn't buy"
),
sell=(
"you certainly would sell",
"you certainly wouldn't sell"
),
short_sell=(
"you certainly would issue the ticket",
"you certainly would not issue the ticket"
)
)
REFINE_ERROR_MSG = 'Please refine your choice.'
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
group_type = models.StringField()
task = models.StringField()
prospect = models.StringField()
payoff_min = models.IntegerField()
payoff_max = models.IntegerField()
left = models.FloatField()
indifference_lower = models.FloatField()
indifference_upper = models.FloatField()
right = models.FloatField()
refine = models.BooleanField(
label="We used some rounding in the previous screen. Would you like to refine your answers?",
choices=[
(True, "YES"),
(False, "NO"),
],
initial = False,
widget=widgets.RadioSelect
)
left_refined = models.FloatField(
label="I certainly would buy if the price was at or below:",
blank=True
)
indifference_lower_refined = models.FloatField()
indifference_upper_refined = models.FloatField()
right_refined = models.FloatField(
label="I certainly would not buy if the price was at or above:",
blank=True
)
# FUNCTIONS
def creating_session(subsession: Subsession):
ps = subsession.get_players()
round_number = subsession.round_number
if round_number == 1:
from random import sample
from itertools import cycle, product
specs = {
C.GROUPS[0]: list(product([C.GROUPS[0]], [C.PROSPECTS[0]], C.PAYOFF_PAIRS_IDS)),
C.GROUPS[1]: list(product([C.GROUPS[1]], [C.PROSPECTS[2]], C.PAYOFF_PAIRS_IDS)),
C.GROUPS[2]: list(product([C.GROUPS[2]], C.PROSPECTS, [C.PAYOFF_PAIRS_IDS[0]]))
}
groups = cycle(sample(C.GROUPS, k=len(C.GROUPS)))
for p in ps:
group = next(groups)
if group in C.GROUPS[:2]:
pspecs = []
tasks = sample(C.TASKS, k=len(C.TASKS))
sample_specs = sample(specs[group], k=len(C.PAYOFF_PAIRS_IDS))
for t in tasks:
for s in sample_specs:
s_list = list(s)
s_list.append(t)
pspecs.append(s_list)
pspecs.append(s_list)
pspecs.append(s_list)
pspecs.append(s_list)
p.participant.vars['specs'] = pspecs
else:
pspecs = []
tasks = sample(C.TASKS, k=len(C.TASKS))
sample_specs = sample(specs[group], k=len(C.TASKS))
for t in tasks:
for s in sample_specs:
s_list = list(s)
s_list.append(t)
pspecs.append(s_list)
p.participant.vars['specs'] = pspecs
for p in ps:
p.group_type, p.prospect, payoff_pair_id, p.task = p.participant.vars['specs'][round_number - 1]
p.payoff_min, p.payoff_max = C.PAYOFF_PAIRS[payoff_pair_id][0], C.PAYOFF_PAIRS[payoff_pair_id][1]
def include_slider_field(name: str, label: str, min, max, min_label: str = '', max_label: str = '', step=1,
no_default=True, default=None, display_output=True) -> dict:
"""
Custom Slider field for oTree
:param name: name of the field as defined under the Player, Group, or Subsession classes
:param label: label of the field
:param min: minimum accepted value of the field as defined under the Player, Group, or Subsession classes
:param max: maximum accepted value of the field as defined under the Player, Group, or Subsession classes
:param min_label: label to display as minimum extremum
:param max_label: label to display as maximum extremum
:param step: slider's step attribute
:param no_default: if the slider's thumb is hidden in the default position
:param default: default value displayed on the slider
:param display_output: if the slider's current value is displayed as output
:return: HTML of the widget
"""
no_default_class = 'otree-plus-slider-no-default' if no_default else ''
on_click = 'this.classList.remove("otree-plus-slider-no-default")' if no_default else ''
display_output_class = 'otree-plus-slider-display-output' if display_output else ''
return dict(
url='mpl_table/includes/slider_field.html',
params=locals()
)
def enforce_monotonicity(player: Player, values: dict) -> str:
if 'left_refined' in values and 'right_refined' in values and values['left_refined'] >= values['right_refined']:
return player.session.config['monotonicity_error']
def get_mpl_table_params(player, config: dict) -> dict:
if player.payoff_min == 300:
mpl_step = int(config['mpl_step']/2)
else :
mpl_step = config['mpl_step']
return dict(
labels=C.MPL_TABLE_HEADERS[player.task],
list=[
(
f'{v:.0f} {config["currency_symbol"]}' if float(v).is_integer() else f'{v:.2f} {config["currency_symbol"]}',
f'',
f'',
f'',
)
for n, v in enumerate(range(player.payoff_min, player.payoff_max + mpl_step, mpl_step))
]
)
def get_left_and_right_formatted(player: Player, config: dict) -> tuple[str, str]:
left = player.left
right = player.right
return (
f'{left:.0f} {config["currency_symbol"]}' if float(left).is_integer() else f'{left:.2f} {config["currency_symbol"]}',
f'{right:.0f} {config["currency_symbol"]}' if float(right).is_integer() else f'{right:.2f} {config["currency_symbol"]}'
)
def left_refined_min(player: Player):
return player.left if player.task == C.TASKS[0] else player.left - player.session.config['mpl_refine_margin']
def left_refined_max(player: Player):
return player.left + player.session.config['mpl_refine_margin'] if player.task == C.TASKS[0] else player.left
def right_refined_min(player: Player):
return player.right - player.session.config['mpl_refine_margin'] if player.task == C.TASKS[0] else player.right
def right_refined_max(player: Player):
return player.right if player.task == C.TASKS[0] else player.right + player.session.config['mpl_refine_margin']
# PAGES
class Instructions(Page):
@staticmethod
def vars_for_template(player: Player):
if player.group_type == 'C':
title_desc=f'{C.PROSPECTS_DICT[player.prospect]}'
else:
title_desc=f'{player.payoff_max}-{player.payoff_min}'
return dict(
title = f'{C.TASK_DICT[player.task]} a ticket',
coupon_name = title_desc,
path_to_instructions=f'mpl_table/includes/{player.task}.html',
urn_img=f'mpl_table/imgs/urn_{player.prospect}.jpg'
)
@staticmethod
def is_displayed(player: Player):
round_number = player.round_number
if (round_number<7 and player.group_type!=C.GROUPS[2]) or player.group_type==C.GROUPS[2]:
return True
else:
return False
class Task(Page):
@staticmethod
def vars_for_template(player: Player):
return dict(
path_to_instructions=f'mpl_table/includes/{player.task}.html',
urn_img=f'mpl_table/imgs/urn_{player.prospect}.jpg'
)
class MPLTable(Page):
form_model = 'player'
form_fields = ['left', 'right']
@staticmethod
def vars_for_template(player: Player):
if player.group_type == 'C':
title_desc=f'{C.PROSPECTS_DICT[player.prospect]}'
else:
title_desc=f'{player.payoff_max}-{player.payoff_min}'
return dict(
title = f'{C.TASK_DICT[player.task]} a ticket',
coupon_name = title_desc,
mpl_table_params=get_mpl_table_params(player, player.session.config)
)
@staticmethod
def js_vars(player: Player):
config = player.session.config
if player.payoff_min == 300:
mpl_step = config['mpl_step']
mpl_step = int(mpl_step/2)
else :
mpl_step = config['mpl_step']
return dict(
mpl_table_values=list(range(player.payoff_min, player.payoff_max + mpl_step, mpl_step)),
left_down=player.task == C.TASKS[0]
)
@staticmethod
def error_message(player: Player, values):
pass
@staticmethod
def is_displayed(player: Player):
round_number = player.round_number
if (round_number<7 and player.group_type!=C.GROUPS[2]) or player.group_type==C.GROUPS[2]:
return True
else:
return False
# @staticmethod
# def app_after_this_page(player: Player):
# if player.group_type != C.GROUPS[2] and player.round_number==6:
# return "end"
@staticmethod
def before_next_page(player: Player, timeout_happened):
if player.payoff_min == 300:
mpl_step = int(player.session.config['mpl_step']/2)
else :
mpl_step = player.session.config['mpl_step']
if player.task == C.TASKS[0]:
if player.right - player.left > 0:
player.indifference_lower, player.indifference_upper = player.left + mpl_step, player.right - mpl_step
else:
player.indifference_lower, player.indifference_upper = 0, 0
else:
if player.left - player.right > 0:
player.indifference_lower, player.indifference_upper = player.right + mpl_step, player.left - mpl_step
else:
player.indifference_lower, player.indifference_upper = 0, 0
class MPLRefine(Page):
form_model = 'player'
@staticmethod
def get_form_fields(player: Player):
form_fields = ['refine']
if player.left > 0:
form_fields.append('left_refined')
if player.right > 0:
form_fields.append('right_refined')
return form_fields
@staticmethod
def vars_for_template(player: Player):
config = player.session.config
left, right = get_left_and_right_formatted(player, config)
left_refined_min_val = f'{left_refined_min(player):.2f}'.replace(',', '.').replace('.00', '')
left_refined_max_val = f'{left_refined_max(player):.2f}'.replace(',', '.').replace('.00', '')
right_refined_min_val = f'{right_refined_min(player):.2f}'.replace(',', '.').replace('.00', '')
right_refined_max_val = f'{right_refined_max(player):.2f}'.replace(',', '.').replace('.00', '')
task = player.task if player.task in C.TASKS[:2] else 'issue the ticket'
left_down = player.task == C.TASKS[0]
left_direction = 'below' if left_down else 'above'
right_direction = 'above' if left_down else 'below'
has_indifference = player.right == 0 or player.right - player.left > config['mpl_step'] if left_down else \
player.left == 0 or player.left - player.right > config['mpl_step']
if player.group_type == 'C':
title_desc=f'{C.TASK_DICT[player.task]} a ticket {C.PROSPECTS_DICT[player.prospect]}'
else:
title_desc=f'{C.TASK_DICT[player.task]} a ticket {player.payoff_max}-{player.payoff_min}'
return dict(
title=title_desc,
mpl_feedback=C.MPL_FEEDBACK[player.task],
has_indifference=has_indifference,
left=left,
left_direction=left_direction,
right=right,
right_direction=right_direction,
left_refined=include_slider_field(
name='left_refined',
label=f'I certainly would {task} if the price was at or {left_direction}:',
min=left_refined_min_val,
max=left_refined_max_val,
min_label=f'{left_refined_min_val} {config["currency_symbol"]}',
max_label=f'{left_refined_max_val} {config["currency_symbol"]}',
step=config['mpl_refine_step'],
no_default=False,
default=left_refined_min_val if left_down else left_refined_max_val
),
right_refined=include_slider_field(
name='right_refined',
label=f"I certainly would not {task} if the price was at or {right_direction}:",
min=right_refined_min_val,
max=right_refined_max_val,
min_label=f'{right_refined_min_val} {config["currency_symbol"]}',
max_label=f'{right_refined_max_val} {config["currency_symbol"]}',
step=config['mpl_refine_step'],
no_default=False,
default=right_refined_max_val if left_down else right_refined_min_val
)
)
@staticmethod
def js_vars(player: Player):
return dict(
currency_symbol=player.session.config["currency_symbol"]
)
@staticmethod
def error_message(player: Player, values):
errors = dict()
if not values['refine']:
if 'left_refined' in values and values['left_refined'] is None:
errors['left_refined'] = C.REFINE_ERROR_MSG
if 'right_refined' in values and values['right_refined'] is None:
errors['right_refined'] = C.REFINE_ERROR_MSG
if errors:
return errors
else:
enforce_monotonicity(player, values)
page_sequence = [Instructions, MPLTable]