from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'app' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 BONUS=0.2 GAME_DURATION = 600 # 10 minutes in seconds class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): identification = models.LongStringField() choice = models.StringField(widget=widgets.RadioSelect) question_1 = models.IntegerField( label = "If I guess the lucky number, I will win " + str(C.BONUS) + " euros.", choices=[[1, 'True'], [2, 'False']], widget=widgets.RadioSelectHorizontal, ) question_2 = models.IntegerField( label="The lucky number is the same for every participant in this study.", choices=[[1, 'True'], [2, 'False']], widget=widgets.RadioSelectHorizontal, ) question_3 = models.IntegerField( label="If I participated in this study tomorrow rather than today, the lucky number could be different from today.", choices=[[1, 'True'], [2, 'False']], widget=widgets.RadioSelectHorizontal, ) consent_given = models.BooleanField(label="") question_1_incorrect_answers = models.PositiveIntegerField(initial=0) question_2_incorrect_answers = models.PositiveIntegerField(initial=0) question_3_incorrect_answers = models.PositiveIntegerField(initial=0) understanding_questions_wrong_attempts = models.PositiveIntegerField(initial=0) chose_lucky = models.BooleanField() # A/B Pressing Game fields ab_score = models.IntegerField(initial=0) ab_sequence = models.StringField(initial="") # Track current sequence (e.g., "A", "AB", "ABA") ab_game_started = models.BooleanField(initial=False) ab_game_finished = models.BooleanField(initial=False) ab_total_presses = models.IntegerField(initial=0) ab_game_start_time = models.FloatField(initial=0) # Server-side start time # Condition assignment condition = models.IntegerField(choices=[[1, 'No payment effect'], [2, 'Red Cross donation'], [3, 'Self-select Red Cross']]) # Self-selection choice (only for condition 3) self_select_choice = models.IntegerField(choices=[[1, 'Keep bonus, no donation'], [2, 'Donate bonus to Red Cross']]) def choice_choices(player): import random choices=C.NUMBERS.copy() random.shuffle(choices) return choices def assign_condition(player): import random if not player.field_maybe_none('condition'): session_config = player.subsession.session.config player.participant.self_select_choice = -100 # Check session configuration variables if session_config.get('random') == 1: # Randomly assign between condition 1 and 2 player.condition = random.choice([1, 2]) elif session_config.get('self_select') == 1: # Always assign condition 3 (self-select) player.condition = 3 elif session_config.get('fix_pay') == 1: # Always assign condition 1 (no payment effect) player.condition = 1 elif session_config.get('donation') == 1: # Always assign condition 2 (Red Cross donation) player.condition = 2 else: # Default fallback - random assignment player.condition = random.choice([1, 2, 3]) class quiz(Page): @staticmethod def is_displayed(player: Player): return player.consent == 1 form_model = 'player' form_fields = ['question_1','question_2','question_3'] @staticmethod def error_message(player, values): solutions = dict( question_1=C.CORRECT_1, question_2=C.CORRECT_2, question_3=C.CORRECT_3, ) hints = dict( question_1=C.HINT_1, question_2=C.HINT_2, question_3=C.HINT_3, ) question_names = dict( question_1='question_1_incorrect_answers', question_2='question_2_incorrect_answers', question_3='question_3_incorrect_answers', ) error_messages = dict() for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages[field_name] = hints[field_name] setattr(player, question_names[field_name], getattr(player, question_names[field_name])+1) player.understanding_questions_wrong_attempts=player.understanding_questions_wrong_attempts+1 return error_messages class ab_pressing_game(Page): # No timeout - server-side timer handles everything @staticmethod def vars_for_template(player: Player): return dict( game_duration=C.GAME_DURATION, condition=player.condition, self_select_choice=player.field_maybe_none('self_select_choice') ) @staticmethod def live_method(player, data): import time # Handle different types of messages if 'action' in data: action = data['action'] if action == 'start_game': import time player.ab_game_started = True player.ab_sequence = "" player.ab_score = 0 player.ab_total_presses = 0 player.ab_game_finished = False player.ab_game_start_time = time.time() return {player.id_in_group: { 'type': 'game_started', 'score': player.ab_score, 'sequence': player.ab_sequence }} elif action == 'press' and 'button' in data: button = data['button'] if not player.ab_game_started or player.ab_game_finished: return {player.id_in_group: {'type': 'error', 'message': 'Game not started or finished'}} # Update total presses player.ab_total_presses += 1 # Check if this completes an A-B sequence if button == 'A': if player.ab_sequence == "" or player.ab_sequence.endswith('B'): player.ab_sequence = "A" else: # Invalid press - reset sequence player.ab_sequence = "A" elif button == 'B': if player.ab_sequence == "A": # Complete A-B sequence! Award point player.ab_score += 1 player.ab_sequence = "" else: # Invalid press - reset sequence player.ab_sequence = "" return {player.id_in_group: { 'type': 'press_result', 'button': button, 'score': player.ab_score, 'sequence': player.ab_sequence, 'total_presses': player.ab_total_presses }} elif 'get_time' in data: # Calculate remaining time based on server start time import time remaining_time = C.GAME_DURATION if player.ab_game_started and not player.ab_game_finished and player.ab_game_start_time > 0: elapsed = time.time() - player.ab_game_start_time remaining_time = max(0, C.GAME_DURATION - elapsed) # Auto-finish when time is up if remaining_time <= 0: player.ab_game_finished = True return {player.id_in_group: { 'type': 'time_update', 'remaining_time': int(remaining_time), 'game_finished': player.ab_game_finished }} elif 'get_state' in data: # Return current game state return {player.id_in_group: { 'type': 'game_state', 'score': player.ab_score, 'sequence': player.ab_sequence, 'game_started': player.ab_game_started, 'game_finished': player.ab_game_finished, 'total_presses': player.ab_total_presses }} return {} class final_page(Page): @staticmethod def vars_for_template(player: Player): # Calculate Red Cross donation amount (10 cents per 100 points, only in increments of 100 points) red_cross_donation_cents = (player.ab_score // 100) * 10 red_cross_donation_euros = red_cross_donation_cents / 100 red_cross_donation_formatted = f"{red_cross_donation_euros:.2f}" # Determine effective condition based on self-selection effective_condition = player.condition if player.condition == 3: effective_condition = player.field_maybe_none('self_select_choice') return dict( pay=player.participant.payoff_plus_participation_fee(), ab_score=player.ab_score, ab_total_presses=player.ab_total_presses, condition=player.condition, effective_condition=effective_condition, self_select_choice=player.field_maybe_none('self_select_choice'), red_cross_donation=red_cross_donation_formatted ) class ConsentPage(Page): form_model = 'player' form_fields = ['consent_given'] @staticmethod def is_displayed(player: Player): return not ('skip_quiz' in player.session.config) class ConsentNotGiven(Page): @staticmethod def is_displayed(player: Player): return not ('skip_quiz' in player.session.config) and not player.consent_given class instructions(Page): @staticmethod def vars_for_template(player): # Assign condition if not already assigned assign_condition(player) return dict( condition=player.condition ) class self_selection(Page): @staticmethod def is_displayed(player): return player.condition == 3 form_model = 'player' form_fields = ['self_select_choice'] @staticmethod def before_next_page(player, timeout_happened): # Save self_select_choice to participant field player.participant.self_select_choice = player.self_select_choice @staticmethod def vars_for_template(player): return dict( condition=player.condition ) page_sequence = [ConsentPage, ConsentNotGiven,instructions, self_selection, ab_pressing_game, final_page]