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]