from otree.api import * import pandas as pd import random import time import json doc = """ Your app description """ def open_CSV(filename): temp = pd.read_csv(filename, sep=',') return temp # MODELS class C(BaseConstants): NAME_IN_URL = 'voting' PLAYERS_PER_GROUP = None N_STRATEGIES = 4 N_GOVERNANCE = 2 PAYOFFS = open_CSV('voting/staircase_scores.csv') N_GROUP = 4 BRIBED_SIM_COM = 2 BRIBED_COM_SIM = 2 NOT_BRIBED_SIM_COM = 2 NOT_BRIBED_COM_SIM = 2 LONG_WAIT = 10 WAIT_PAY = 1.5 MAX_FORCED = 3 NUM_ROUNDS = 60 # EPOCH is no longer used for round-plan logic; epoch boundaries are encoded # directly in payoffs_for_rounds. Kept here only for reference. EPOCH = 15 BUTTON_LABELS = ["Robot K3M", "Robot N7P", "Robot V2X", "Robot Z5B"] LEADER_BUTTON_LABELS = ["Research Team", "Mission Commander"] TIMEOUT_VALUE_ACTIVE = 60 TIMEOUT_VALUE_INACTIVE = 10 ADMONISH_MSG = [ "WARNING: You gave no response on the previous page. If this happens too often, you will be marked inactive. This may lead to not getting paid.", "You were previously marked inactive. Too many periods of inactivity may lead to not getting paid", ] EARLY_ROUNDS = 3 EXTRA_TIME = 60 FEEDBACK_OUTCOMES = [ "The Research Team decided to send", "The Research Team decided after one revote to send", "The Research Team decided after two revotes to send", "After two revotes the Research Team could not reach a majority. A random robot type was sent:", "The Mission Commander decided to send", ] QUESTIONS_TRIPLE = [ "Question 1: What robot do you personally think would be best for this round?", "🤖 Question 2: What robot would you choose as part of the Research Team for this round?", "🧑‍🔬🎖️ Question 3: Who should decide which type of robot to send for this round?", ] class Subsession(BaseSubsession): pass def creating_session(subsession): session = subsession.session if subsession.round_number == 1: session.bribed_sim_com = 0 session.bribed_com_sim = 0 session.not_bribed_sim_com = 0 session.not_bribed_com_sim = 0 def long_wait(player): return time.time() - player.participant.wait_page_arrival > C.LONG_WAIT * 60 def group_by_arrival_time_method(subsession, waiting_players): session = subsession.session long_waiting = [p for p in waiting_players if long_wait(p)] if len(long_waiting) >= 1: for player in long_waiting: print('Ready to let participant go, waiting for too long') player.participant.is_single = True player.payoff += C.WAIT_PAY print("Here is their total calculated payoff: ", player.participant.payoff_plus_participation_fee()) return [player] if len(waiting_players) >= C.N_GROUP: print('Ready to create a group of 4') return waiting_players[:C.N_GROUP] def vote_field(label_text="Below are the options you may choose from. To vote, select one of the options.", num_options=C.N_STRATEGIES): return models.IntegerField( label=label_text, widget=widgets.RadioSelect(), choices=[[i, str(i)] for i in range(1, num_options + 1)], ) class Group(BaseGroup): representative_democracy = models.BooleanField() governance_direct_tally = models.IntegerField() governance_repres_tally = models.IntegerField() governance_no_majority = models.BooleanField(initial=False) strategy_no_majority = models.BooleanField(initial=False) winner_strategy = models.IntegerField(initial=-1) failed_majority = models.BooleanField(initial=False) majority_message = models.StringField(initial="TEST: NO MAJ MSG") treatment_bribed_leader = models.BooleanField() treatment_simple_complex = models.BooleanField() has_N_GROUP_players = models.BooleanField() # JSON list of 60 [index, complexity] pairs, one per round. # Replaces shuffle_list and current_shuffle. payoffs_for_rounds = models.LongStringField() def get_option_label(option_number): if 1 <= option_number <= len(C.BUTTON_LABELS): return C.BUTTON_LABELS[option_number - 1] return "Invalid option number" def count_treatment(group: Group): if group.treatment_bribed_leader and group.treatment_simple_complex: group.subsession.session.bribed_sim_com += 1 if not group.treatment_bribed_leader and group.treatment_simple_complex: group.subsession.session.not_bribed_sim_com += 1 if group.treatment_bribed_leader and not group.treatment_simple_complex: group.subsession.session.bribed_com_sim += 1 if not group.treatment_bribed_leader and not group.treatment_simple_complex: group.subsession.session.not_bribed_com_sim += 1 def bribe_simpcomp(group: Group): group.treatment_bribed_leader = True group.treatment_simple_complex = True count_treatment(group) def bribe_compsimp(group: Group): group.treatment_bribed_leader = True group.treatment_simple_complex = False count_treatment(group) def not_bribe_simpcomp(group: Group): group.treatment_bribed_leader = False group.treatment_simple_complex = True count_treatment(group) def not_bribe_compsimp(group: Group): group.treatment_bribed_leader = False group.treatment_simple_complex = False count_treatment(group) def random_treatment(group: Group): group.treatment_bribed_leader = random.choice([True, False]) group.treatment_simple_complex = random.choice([True, False]) count_treatment(group) class Player(BasePlayer): strategy_vote_private = vote_field() strategy_vote_0 = vote_field() strategy_vote_1 = vote_field() strategy_vote_2 = vote_field() strategy_leader = vote_field() # Deprecated representative_democracy = models.BooleanField() resurrect_me = models.BooleanField(initial=False) forced_submit = models.BooleanField(initial=False) adm_msg_idx = models.IntegerField(initial=-1) score = models.FloatField(initial=-1.0) speed = models.IntegerField(initial=-1) round_complexity = models.StringField() # For data export which_inactive = models.IntegerField() ## FUNCTIONS ## def increment_votes(player: Player): player.participant.votes += 1 def increment_forced(player: Player): player.participant.local_forced_submits += 1 player.participant.global_forced_submits += 1 player.participant.admonish = True player.adm_msg_idx = 0 if player.participant.local_forced_submits >= C.MAX_FORCED: player.participant.active = False player.participant.admonish = False print("Function incremented their forced choices!") def handle_resurrections(player: Player): if not player.resurrect_me: print("Resurrect me is False.") return player.participant.active = True player.participant.resurrections += 1 player.participant.local_forced_submits = 0 print("I will be resurrected next page!") player.resurrect_me = False player.participant.admonish = True player.adm_msg_idx = 1 def calculate_speed(player): if player.field_maybe_none('strategy_vote_2') is not None: num_student_votes = 3 elif player.field_maybe_none('strategy_vote_1') is not None: num_student_votes = 2 else: num_student_votes = 1 if player.group.failed_majority: which_maj_outcome_idx = 4 elif player.group.representative_democracy: which_maj_outcome_idx = 0 else: which_maj_outcome_idx = num_student_votes speed_map = {0: 1, 1: 1, 2: 2, 3: 3, 4: 4} speed = speed_map.get(which_maj_outcome_idx, 4) player.speed = speed player.participant.speed = speed def speed_to_penalty(speed): return {1: 0, 2: -5, 3: -10, 4: -15}.get(speed, -999) # ── PAGES ───────────────────────────────────────────────────────────────────── class Grouping_WaitPage(WaitPage): template_name = 'voting/Grouping_WaitPage.html' group_by_arrival_time = True @staticmethod def is_displayed(player): return (player.round_number == 1 and player.participant.active and player.participant.voting_APP) class Experiment_WaitPage(WaitPage): template_name = 'voting/Experiment_WaitPage.html' @staticmethod def after_all_players_arrive(group: Group): session = group.subsession.session subsession = group.subsession print("BRIBED_SIM_COM:", session.bribed_sim_com < C.BRIBED_SIM_COM, '\n', "BRIBED_COM_SIM:", session.bribed_com_sim < C.BRIBED_COM_SIM, '\n', "NOT BRIBED_SIM_COM:", session.not_bribed_sim_com < C.NOT_BRIBED_SIM_COM, '\n', "NOT BRIBED_COM_SIM:", session.not_bribed_com_sim < C.NOT_BRIBED_COM_SIM) if subsession.round_number == 1: group_players = group.get_players() if len(group_players) == 1: group.has_N_GROUP_players = False for player in group_players: player.participant.is_single = True player.participant.pay_APP = True else: group.has_N_GROUP_players = True # Assign treatment sequentially, fall back to random if all slots filled conditions = [ (session.bribed_sim_com < C.BRIBED_SIM_COM, bribe_simpcomp), (session.bribed_com_sim < C.BRIBED_COM_SIM, bribe_compsimp), (session.not_bribed_sim_com < C.NOT_BRIBED_SIM_COM, not_bribe_simpcomp), (session.not_bribed_com_sim < C.NOT_BRIBED_COM_SIM, not_bribe_compsimp), ] for condition, action in conditions: if condition: action(group) break else: random_treatment(group) # Generate the 60-round plan from two independently shuffled epoch orderings. # treatment_simple_complex=True → Simple first (rounds 1–30), Complex second (31–60) # treatment_simple_complex=False → Complex first (rounds 1–30), Simple second (31–60) simple_order = list(range(1, 31)) random.shuffle(simple_order) complex_order = list(range(1, 31)) random.shuffle(complex_order) if group.treatment_simple_complex: payoffs_for_rounds = ( [[idx, "Simple"] for idx in simple_order] + [[idx, "Complex"] for idx in complex_order] ) else: payoffs_for_rounds = ( [[idx, "Complex"] for idx in complex_order] + [[idx, "Simple"] for idx in simple_order] ) group.payoffs_for_rounds = json.dumps(payoffs_for_rounds) print("Round plan (first 5):", payoffs_for_rounds[:5]) # Pre-build each player's payoff cache: a list of 60 dicts, # index = round_number - 1, keys = str(option) (string because # oTree JSON-serialises participant.vars, coercing int keys to str). # Each entry: {'display': float, 'feedback': float}. # Storing per-participant avoids redundant CSV scans later and # means each player only carries their own PlayerID slice (480 floats). for player in group_players: cache = [] for idx, complexity in payoffs_for_rounds: player_rows = C.PAYOFFS[ (C.PAYOFFS['Complexity'] == complexity) & (C.PAYOFFS['Index'] == idx) & (C.PAYOFFS['PlayerID'] == player.id_in_group) ] round_cache = { str(int(row['Option'])): { 'display': float(row['Display_Score']), 'feedback': float(row['Feedback_Score']), } for _, row in player_rows.iterrows() } cache.append(round_cache) player.participant.vars['payoffs_cache'] = cache print(f"Cache built for PlayerID {player.id_in_group}, " f"round 1 sample: {cache[0]}") else: # Rounds 2–60: propagate group-level fields from round 1 group.treatment_bribed_leader = group.in_round(1).treatment_bribed_leader group.treatment_simple_complex = group.in_round(1).treatment_simple_complex group.payoffs_for_rounds = group.in_round(1).payoffs_for_rounds # payoffs_cache lives on participant, so no action needed here @staticmethod def is_displayed(player): if player.round_number == 1: return player.participant.voting_APP return player.participant.voting_APP and not player.participant.is_single class ThreeVotes(Page): def get_timeout_seconds(player): base = C.TIMEOUT_VALUE_ACTIVE if player.participant.active else C.TIMEOUT_VALUE_INACTIVE extra = C.EXTRA_TIME if player.round_number < C.EARLY_ROUNDS else 0 return base + extra form_model = 'player' form_fields = ['strategy_vote_0', 'representative_democracy', 'resurrect_me', 'strategy_vote_private'] @staticmethod def vars_for_template(player): print("Labels:", player.participant.vars['button_labels']) print("Values:", player.participant.vars['button_values']) # Look up Display_Score for each of the four options for this player # in this round. button_values holds the actual option numbers in the # order they are displayed, so score_N correctly corresponds to button_N. # Cache keys are strings (see cache-building note in Experiment_WaitPage). cache = player.participant.vars['payoffs_cache'] round_cache = cache[player.round_number - 1] btn_vals = player.participant.vars['button_values'] score_1 = int(float(round_cache[str(btn_vals[0])]['display'])) score_2 = int(float(round_cache[str(btn_vals[1])]['display'])) score_3 = int(float(round_cache[str(btn_vals[2])]['display'])) score_4 = int(float(round_cache[str(btn_vals[3])]['display'])) return dict( button_1 = C.BUTTON_LABELS[0], button_2 = C.BUTTON_LABELS[1], button_3 = C.BUTTON_LABELS[2], button_4 = C.BUTTON_LABELS[3], button_TA = C.LEADER_BUTTON_LABELS[0], button_prof = C.LEADER_BUTTON_LABELS[1], admonish_msg = C.ADMONISH_MSG[player.adm_msg_idx], round_num = player.round_number, last_round = C.NUM_ROUNDS, score_1 = score_1, score_2 = score_2, score_3 = score_3, score_4 = score_4, ) @staticmethod def before_next_page(player: Player, timeout_happened): increment_votes(player) player.participant.admonish = False if timeout_happened: player.strategy_vote_0 = random.choice(range(1, C.N_STRATEGIES + 1)) player.representative_democracy = random.choice([True, False]) player.strategy_vote_private = random.choice(range(1, C.N_STRATEGIES + 1)) player.forced_submit = True player.which_inactive = 1 increment_forced(player) handle_resurrections(player) @staticmethod def is_displayed(player): return player.participant.voting_APP and not player.participant.is_single class ThreeVotes_WaitPage(WaitPage): template_name = 'voting/waitpage_base.html' @staticmethod def after_all_players_arrive(group: Group): # GOVERNANCE TALLY players = group.get_players() direct = sum(not p.representative_democracy for p in players) representative = len(players) - direct group.governance_direct_tally = direct group.governance_repres_tally = representative if direct != representative: group.representative_democracy = representative > direct else: group.governance_no_majority = True group.representative_democracy = random.choice([False, True]) # STRATEGY TALLY (first vote) strategy_tally = {i: 0 for i in range(1, C.N_STRATEGIES + 1)} for player in players: strategy_tally[player.strategy_vote_0] += 1 options = list(strategy_tally.keys()) votes = list(strategy_tally.values()) random_cast = random.choices(options, weights=votes, k=1)[0] # noqa: F841 for player in players: player.participant.vars['strategy_tally'] = {'V0': {}, 'V1': {}, 'V2': {}} for player in players: player.participant.vars['strategy_tally']['V0'] = strategy_tally majority_threshold = (C.N_GROUP / 2) + 1 strategy_with_majority = [k for k, v in strategy_tally.items() if v >= majority_threshold] if len(strategy_with_majority) == 1: group.winner_strategy = strategy_with_majority[0] group.strategy_no_majority = False else: group.strategy_no_majority = True # LEADER DECISION if group.representative_democracy: group.majority_message = ("Leader did not make this decision, " "and this text should not be displayed.") # Derive this round's index and complexity from the precomputed round plan payoffs_for_rounds = json.loads(group.in_round(1).payoffs_for_rounds) round_number = players[0].round_number shuffle_idx, current_complexity = payoffs_for_rounds[round_number - 1] filtered_payoffs = C.PAYOFFS[ (C.PAYOFFS['Complexity'] == current_complexity) & (C.PAYOFFS['Index'] == shuffle_idx) ] if not filtered_payoffs.empty: # Rank options by average Feedback_Score across all PlayerIDs. # DESIGN DECISION: the leader uses Feedback_Score as a proxy for # true option quality (omniscient-leader assumption). This does NOT # reflect what participants see, which is drawn from Display_Score. avg_scores = ( filtered_payoffs .groupby('Option')['Feedback_Score'] .mean() .reset_index() .rename(columns={'Feedback_Score': 'AvgScore'}) .sort_values('AvgScore', ascending=False) .reset_index(drop=True) ) if group.treatment_bribed_leader: group.majority_message = "The Professor made the decision." print(f"\n=== BRIBED LEADER (Complexity: {current_complexity}, " f"Index: {shuffle_idx}) ===") for i, row in avg_scores.iterrows(): rank = ["Best", "2nd Best", "3rd Best", "Worst"][i] if i < 4 else f"{i+1}th" print(f" Option {int(row['Option'])}: " f"AvgFeedback={row['AvgScore']:.3f} ({rank})") # Bribed leader: 3rd best (70%) or worst (30%) if len(avg_scores) >= 4: third_best = avg_scores.iloc[2]['Option'] worst = avg_scores.iloc[3]['Option'] chosen_option = random.choices( [third_best, worst], weights=[0.7, 0.3], k=1 )[0] choice_type = "3rd Best (70%)" if chosen_option == third_best else "Worst (30%)" print(f">>> SELECTED: Option {int(chosen_option)} ({choice_type})") else: chosen_option = avg_scores.iloc[-1]['Option'] print(f"!!! WARNING: Only {len(avg_scores)} options — selected worst.") group.winner_strategy = int(chosen_option) print(f"Final winner_strategy: {group.winner_strategy}\n") else: group.majority_message = "The Professor made the decision." print(f"\n=== NON-BRIBED LEADER (Complexity: {current_complexity}, " f"Index: {shuffle_idx}) ===") for i, row in avg_scores.iterrows(): rank = ["Best", "2nd Best", "3rd Best", "Worst"][i] if i < 4 else f"{i+1}th" print(f" Option {int(row['Option'])}: " f"AvgFeedback={row['AvgScore']:.3f} ({rank})") # Non-bribed leader: best (30%) or 2nd best (70%) if len(avg_scores) >= 2: best = avg_scores.iloc[0]['Option'] second_best = avg_scores.iloc[1]['Option'] chosen_option = random.choices( [best, second_best], weights=[0.3, 0.7], k=1 )[0] choice_type = "Best (30%)" if chosen_option == best else "2nd Best (70%)" print(f">>> SELECTED: Option {int(chosen_option)} ({choice_type})") else: chosen_option = avg_scores.iloc[0]['Option'] print(f"!!! WARNING: Only {len(avg_scores)} options — selected best.") group.winner_strategy = int(chosen_option) print(f"Final winner_strategy: {group.winner_strategy}\n") else: print("!!! ERROR: No filtered payoffs found for leader decision!") else: print("Majority decision to be made") group.majority_message = "the Teaching Assistants decided on the rule." @staticmethod def is_displayed(player): return player.participant.voting_APP and not player.participant.is_single class V1(Page): def get_timeout_seconds(player): base = C.TIMEOUT_VALUE_ACTIVE if player.participant.active else C.TIMEOUT_VALUE_INACTIVE extra = C.EXTRA_TIME if player.round_number < C.EARLY_ROUNDS else 0 return base + extra form_model = 'player' form_fields = ['strategy_vote_1', 'resurrect_me'] @staticmethod def vars_for_template(player): prior_vote = player.participant.vars.get('strategy_tally', {}).get('V0', {}) return dict( prior_vote = prior_vote, admonish_msg = C.ADMONISH_MSG[player.adm_msg_idx], ) @staticmethod def before_next_page(player: Player, timeout_happened): increment_votes(player) player.participant.admonish = False if timeout_happened: player.strategy_vote_1 = random.choice(range(1, C.N_STRATEGIES + 1)) player.forced_submit = True player.which_inactive = 2 increment_forced(player) handle_resurrections(player) @staticmethod def is_displayed(player: Player): if player.participant.voting_APP and not player.participant.is_single: return not player.group.representative_democracy and player.group.strategy_no_majority class V1_WaitPage(WaitPage): template_name = 'voting/waitpage_base.html' @staticmethod def after_all_players_arrive(group: Group): strategy_tally = {i: 0 for i in range(1, C.N_STRATEGIES + 1)} for player in group.get_players(): strategy_tally[player.strategy_vote_1] += 1 majority_threshold = (C.N_GROUP / 2) + 1 strategies_with_majority = [k for k, v in strategy_tally.items() if v >= majority_threshold] if len(strategies_with_majority) == 1: group.winner_strategy = strategies_with_majority[0] group.strategy_no_majority = False group.majority_message = "TKTK: There was winning majority" else: group.strategy_no_majority = True group.majority_message = "TKTK: There was no winning majority" for player in group.get_players(): player.participant.vars['strategy_tally']['V1'] = strategy_tally @staticmethod def is_displayed(player: Player): if player.participant.voting_APP and not player.participant.is_single: return not player.group.representative_democracy and player.group.strategy_no_majority class V2(Page): def get_timeout_seconds(player): base = C.TIMEOUT_VALUE_ACTIVE if player.participant.active else C.TIMEOUT_VALUE_INACTIVE extra = C.EXTRA_TIME if player.round_number < C.EARLY_ROUNDS else 0 return base + extra form_model = 'player' form_fields = ['strategy_vote_2', 'resurrect_me'] @staticmethod def vars_for_template(player): prior_vote = player.participant.vars.get('strategy_tally', {}).get('V1', {}) return dict( prior_vote = prior_vote, button_1 = C.BUTTON_LABELS[0], button_2 = C.BUTTON_LABELS[1], button_3 = C.BUTTON_LABELS[2], button_4 = C.BUTTON_LABELS[3], admonish_msg = C.ADMONISH_MSG[player.adm_msg_idx], ) @staticmethod def before_next_page(player: Player, timeout_happened): increment_votes(player) player.participant.admonish = False if timeout_happened: player.strategy_vote_2 = random.choice(range(1, C.N_STRATEGIES + 1)) player.forced_submit = True player.which_inactive = 3 increment_forced(player) handle_resurrections(player) @staticmethod def is_displayed(player: Player): if player.participant.voting_APP and not player.participant.is_single: return not player.group.representative_democracy and player.group.strategy_no_majority class V2_WaitPage(WaitPage): template_name = 'voting/waitpage_base.html' @staticmethod def after_all_players_arrive(group: Group): strategy_tally = {i: 0 for i in range(1, C.N_STRATEGIES + 1)} for player in group.get_players(): strategy_tally[player.strategy_vote_2] += 1 majority_threshold = (C.N_GROUP / 2) + 1 strategies_with_majority = [k for k, v in strategy_tally.items() if v >= majority_threshold] if len(strategies_with_majority) == 1: group.strategy_no_majority = False group.winner_strategy = strategies_with_majority[0] group.majority_message = "TKTK: There was a winning majority." else: group.winner_strategy = random.choice(range(1, C.N_STRATEGIES + 1)) group.failed_majority = True group.strategy_no_majority = False group.majority_message = "After three failed majorities, the Professor chose." print("I chose for them") for player in group.get_players(): player.participant.vars['strategy_tally']['V2'] = strategy_tally @staticmethod def is_displayed(player: Player): if player.participant.voting_APP and not player.participant.is_single: return not player.group.representative_democracy and player.group.strategy_no_majority class Feedback(Page): form_model = 'player' form_fields = ['resurrect_me'] @staticmethod def get_timeout_seconds(player): if not player.participant.active: return 5 return 20 if player.round_number < C.EARLY_ROUNDS else 10 @staticmethod def vars_for_template(player: Player): # Look up this player's Feedback_Score for the winning option in this round. # Cache index = round_number - 1; option key is a string (JSON serialisation). cache = player.participant.vars['payoffs_cache'] round_cache = cache[player.round_number - 1] winner_strategy = player.group.winner_strategy original_score = round(round_cache[str(winner_strategy)]['feedback'], 2) player.score = original_score calculate_speed(player) print("Function speed:", player.speed) penalty = speed_to_penalty(player.speed) final_score = original_score + penalty cumm_score = sum(p.score for p in player.in_previous_rounds()) # noqa: F841 Feedback.winner_strategy_text = get_option_label(player.group.winner_strategy) for key in ['V2', 'V1', 'V0']: if player.participant.vars['strategy_tally'][key]: prior_vote = player.participant.vars['strategy_tally'][key] break if player.group.representative_democracy: leader_choice = player.group.winner_strategy elif player.group.failed_majority: leader_choice = "random" else: leader_choice = "no_leader_choice" # Check speed == 4 FIRST to avoid incorrect outcome display if player.speed == 4: outcome_idx = 3 elif player.group.representative_democracy: outcome_idx = 4 elif player.speed in [1, 2, 3]: outcome_idx = player.speed - 1 else: outcome_idx = 0 Feedback.winner_strategy_text = get_option_label(player.group.winner_strategy) return dict( winner_strategy = Feedback.winner_strategy_text, class_avg = int(original_score), speed_penalty = penalty, total_earned = int(final_score), speed = player.speed, prior_vote = prior_vote, leader_choice = leader_choice, admonish_msg = C.ADMONISH_MSG[player.adm_msg_idx], outcome = C.FEEDBACK_OUTCOMES[outcome_idx], time_limit = Feedback.get_timeout_seconds(player), winner_strategy_value = player.group.winner_strategy # To handle highlighting ) @staticmethod def is_displayed(player: Player): if player.participant.voting_APP and not player.participant.is_single: return True @staticmethod def before_next_page(player, timeout_happened): player.participant.admonish = False # Derive this round's complexity directly from the precomputed round plan. # Replaces the old epoch_index / current_complexity participant vars, # which were fragile and contained a bug (mixing vars[] and direct attributes). payoffs_for_rounds = json.loads(player.group.in_round(1).payoffs_for_rounds) _, player.round_complexity = payoffs_for_rounds[player.round_number - 1] if player.round_number % 10 == 0: player.participant.local_forced_submits = 0 handle_resurrections(player) penalty = speed_to_penalty(player.speed) final_score = player.score + penalty player.payoff = final_score * 0.0025 print("Here is the player payoff:", player.payoff) print("Here is their total calculated payoff:", player.participant.payoff_plus_participation_fee()) print("Current complexity:", player.round_complexity) class Debrief(Page): @staticmethod def is_displayed(player: Player): committed = player.participant.vars.get('committed', False) if not player.participant.is_single: return player.round_number == C.NUM_ROUNDS and committed is True @staticmethod def vars_for_template(player): p = player.participant if p.voting_APP: if p.global_forced_submits > p.votes * 0.4: p.pay_APP = False INACTIVITY = ("You voted less than 40% of the time, and have been deemed " "ineligible for experimental payment. You will be paid for the " "training and any waiting. If you are certain this is incorrect, " "please contact the experimenters through Prolific.") else: INACTIVITY = "TEST: They were active so no need to warn" p.pay_APP = True print("Votes and forced submit for paid:", p.votes, p.global_forced_submits) return dict(pay_explanation=INACTIVITY) page_sequence = [ Grouping_WaitPage, Experiment_WaitPage, ThreeVotes, ThreeVotes_WaitPage, V1, V1_WaitPage, V2, V2_WaitPage, Feedback, Debrief, ]