from otree.api import * import random import math doc = """ PGG with judges + Norms & Empirical Expectations (Pre/Post). """ class C(BaseConstants): NAME_IN_URL = 'base' PLAYERS_PER_GROUP = None NUM_ROUNDS = 15 # Keep 3 for testing, change to 15 later ENDOWMENT = 20 MPCR = 0.4 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): is_judge = models.BooleanField(initial=False) subgroup = models.IntegerField(initial=0) pgg_group = models.IntegerField(initial=0) pid_input = models.StringField(initial='') # PGG fields contribution = models.IntegerField(min=0, max=20, initial=0) payoff_pre = models.FloatField(initial=0.0) punishment_received = models.IntegerField(initial=0) group_punishment_cost = models.FloatField(initial=0.0) # Judge Fields p1_punish = models.IntegerField(min=0, max=5, initial=0) p2_punish = models.IntegerField(min=0, max=5, initial=0) p3_punish = models.IntegerField(min=0, max=5, initial=0) p4_punish = models.IntegerField(min=0, max=5, initial=0) p5_punish = models.IntegerField(min=0, max=5, initial=0) p6_punish = models.IntegerField(min=0, max=5, initial=0) p7_punish = models.IntegerField(min=0, max=5, initial=0) p8_punish = models.IntegerField(min=0, max=5, initial=0) p9_punish = models.IntegerField(min=0, max=5, initial=0) p10_punish = models.IntegerField(min=0, max=5, initial=0) p11_punish = models.IntegerField(min=0, max=5, initial=0) p12_punish = models.IntegerField(min=0, max=5, initial=0) # --- Norms Pre Fields --- np_0 = models.IntegerField(label="Avg contrib 0:", min=0, max=20) np_5 = models.IntegerField(label="Avg contrib 5:", min=0, max=20) np_10 = models.IntegerField(label="Avg contrib 10:", min=0, max=20) np_15 = models.IntegerField(label="Avg contrib 15:", min=0, max=20) np_20 = models.IntegerField(label="Avg contrib 20:", min=0, max=20) nn_0 = models.IntegerField(label="Avg contrib 0:", min=0, max=20) nn_5 = models.IntegerField(label="Avg contrib 5:", min=0, max=20) nn_10 = models.IntegerField(label="Avg contrib 10:", min=0, max=20) nn_15 = models.IntegerField(label="Avg contrib 15:", min=0, max=20) nn_20 = models.IntegerField(label="Avg contrib 20:", min=0, max=20) expected_contribution = models.IntegerField(label="Avg contribution session:", min=0, max=20) # --- Norms Post Fields --- np_0_post = models.IntegerField(label="Avg contrib 0:", min=0, max=20) np_5_post = models.IntegerField(label="Avg contrib 5:", min=0, max=20) np_10_post = models.IntegerField(label="Avg contrib 10:", min=0, max=20) np_15_post = models.IntegerField(label="Avg contrib 15:", min=0, max=20) np_20_post = models.IntegerField(label="Avg contrib 20:", min=0, max=20) nn_0_post = models.IntegerField(label="Avg contrib 0:", min=0, max=20) nn_5_post = models.IntegerField(label="Avg contrib 5:", min=0, max=20) nn_10_post = models.IntegerField(label="Avg contrib 10:", min=0, max=20) nn_15_post = models.IntegerField(label="Avg contrib 15:", min=0, max=20) nn_20_post = models.IntegerField(label="Avg contrib 20:", min=0, max=20) expected_contribution_post = models.IntegerField(label="Avg contribution session:", min=0, max=20) # --- NEW FIELDS FOR DATA EXPORT & PAYMENT --- chosen_level_pre = models.IntegerField(initial=0) chosen_level_post = models.IntegerField(initial=0) avg_personal_norm_pre = models.FloatField(initial=0.0) avg_personal_norm_post = models.FloatField(initial=0.0) avg_contribution_session = models.FloatField(initial=0.0) bonus_norm_pre = models.CurrencyField(initial=0) bonus_norm_post = models.CurrencyField(initial=0) bonus_emp_pre = models.CurrencyField(initial=0) bonus_emp_post = models.CurrencyField(initial=0) pgg_czk = models.CurrencyField(initial=0) # --- NEW FIELDS FOR PART 1 DATA --- p1_cooperate_PD = models.BooleanField() p1_cooperate_Cond_PD_C = models.BooleanField() p1_cooperate_Cond_PD_D = models.BooleanField() p1_boxes_collected = models.IntegerField() p1_bomb = models.IntegerField() p1_epper_scenario = models.StringField() p1_epper_choice = models.IntegerField() epper_bonus = models.CurrencyField(initial=0) epper_bonus_other = models.CurrencyField(initial=0) # --- OUTPUT FIELDS FOR FINAL PAYMENT --- pd_payoff_final = models.CurrencyField(initial=0) bret_payoff_final = models.CurrencyField(initial=0) epper_payoff_final = models.CurrencyField(initial=0) # --- Payment Fields --- paid_rounds_str = models.StringField(initial="") pgg_payoff_sum = models.CurrencyField(initial=0) pgg_czk_total = models.CurrencyField(initial=0) norms_total_bonus = models.CurrencyField(initial=0) final_payment_czk = models.CurrencyField(initial=0) # Demographics age = models.IntegerField(label="What is your age?", min=18, max=99) gender = models.StringField( label="What is your gender?", choices=["Male", "Female", "Non-binary", "Prefer not to say"] ) field_of_study = models.StringField(label="What is your field of study?", blank=True) # Payment Totals pgg_part_total_czk = models.CurrencyField(initial=0) # Sum of PGG + Norms part1_points_total = models.CurrencyField(initial=0) # Sum of PD + Epper + BRET (Points) part1_czk = models.CurrencyField(initial=0) # Points / 30 grand_total_czk = models.CurrencyField(initial=0) # Cash-out amount ### Testing for norms - prefills numbers , comment for the real code # def creating_session(subsession: Subsession): # levels = [0, 5, 10, 15, 20] # # 1. Pre Norms: Generate ONLY in Round 1 # if subsession.round_number == 1: # for p in subsession.get_players(): # for L in levels: # setattr(p, f'np_{L}', random.randint(0, 20)) # setattr(p, f'nn_{L}', random.randint(0, 20)) # p.expected_contribution = random.randint(0, 20) # # 2. Post Norms: Generate ONLY in the Last Round # if subsession.round_number == C.NUM_ROUNDS: # for p in subsession.get_players(): # for L in levels: # setattr(p, f'np_{L}_post', random.randint(0, 20)) # setattr(p, f'nn_{L}_post', random.randint(0, 20)) # p.expected_contribution_post = random.randint(0, 20) # ===================================================== # PAGES # ===================================================== class IDInput(Page): form_model = 'player' form_fields = ['pid_input'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def error_message(player, values): # Use the correct field name for the base app: 'pid_input' input_id = values['pid_input'].strip() # Import DB query tool from otree.database import dbq from pre_survey import PreSurveyProfile # Check if the ID exists in the pre-survey data profile = dbq(PreSurveyProfile).filter_by(user_id=input_id).first() if not profile: return f"ID '{input_id}' not found. Please check your ID from the online part. If your ID is rejected, call the experimenter." if not profile.finished_part_1: return f"ID '{input_id}' exists but did not finish the online part." @staticmethod def before_next_page(player, timeout_happened): from otree.database import dbq from pre_survey import PreSurveyProfile # Use 'pid_input' here too input_id = player.pid_input.strip() # Retrieve the profile again profile = dbq(PreSurveyProfile).filter_by(user_id=input_id).first() # 2. Convert to dict for participant.vars (if you still want that) profile_dict = { 'cooperate_PD': profile.cooperate_PD, 'cooperate_Cond_PD_C': profile.cooperate_Cond_PD_C, 'cooperate_Cond_PD_D': profile.cooperate_Cond_PD_D, 'boxes_collected': profile.boxes_collected, 'bomb': profile.bomb, 'epper_scenario_selected': profile.epper_scenario_selected, 'epper_choice_raw': profile.epper_choice_raw, 'epper_payoff_me': profile.epper_payoff_me, 'epper_payoff_other': profile.epper_payoff_other, 'finished_part_1': profile.finished_part_1, } player.participant.vars['p1_id'] = input_id player.participant.vars['p1_data'] = profile_dict player.participant.label = input_id # Set participant label to ID for easier tracking in admin # 3. Save to player fields player.p1_cooperate_PD = profile.cooperate_PD player.p1_cooperate_Cond_PD_C = profile.cooperate_Cond_PD_C player.p1_cooperate_Cond_PD_D = profile.cooperate_Cond_PD_D player.p1_boxes_collected = profile.boxes_collected player.p1_bomb = profile.bomb player.p1_epper_scenario = profile.epper_scenario_selected player.p1_epper_choice = profile.epper_choice_raw # 4. Process Epper Payoffs player.epper_bonus = abs(profile.epper_payoff_me) player.epper_bonus_other = profile.epper_payoff_other player.participant.vars['epper_bonus'] = player.epper_bonus print(f"SUCCESS: Loaded Part 1 data for {input_id} from database.") class Intro(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): return dict(treatment=player.session.config.get('treatment_type')) class SetupWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): subsession = group.subsession players = subsession.get_players() treatment = subsession.session.config.get('treatment_type') # 1. Round 1: Assign fixed judges and subgroups if subsession.round_number == 1: for p in players: p.participant.vars['is_judge'] = False p.participant.vars['subgroup'] = 0 # ONLY human judge treatments get actual judge players if treatment in ['human_judge', 'humanAI_judge']: # Judges are Player 1 and Player 2 for p in players: if p.id_in_subsession == 1: p.participant.vars['is_judge'] = True p.participant.vars['subgroup'] = 1 elif p.id_in_subsession == 2: p.participant.vars['is_judge'] = True p.participant.vars['subgroup'] = 2 normal_players = [p for p in players if not p.participant.vars['is_judge']] N = len(normal_players) # Split logic valid_splits = [] for a in range(4, N, 4): b = N - a if b >= 4 and b % 4 == 0: valid_splits.append((a, b)) if not valid_splits: if N % 8 == 0: a, b = N//2, N//2 else: raise ValueError(f"Cannot split {N} players into subgroups divisible by 4.") else: a, b = min(valid_splits, key=lambda x: abs(x[0] - x[1])) random.shuffle(normal_players) for p in normal_players[:a]: p.participant.vars['subgroup'] = 1 for p in normal_players[a:a+b]: p.participant.vars['subgroup'] = 2 else: # no_judge AND AI_judge: Everyone is normal, everyone in Subgroup 1 for p in players: p.participant.vars['subgroup'] = 1 # 2. Every Round: Reset values for p in players: p.is_judge = p.participant.vars.get('is_judge', False) p.subgroup = p.participant.vars.get('subgroup', 0) p.pgg_group = 0 # Reset punishment fields p.punishment_received = 0 p.group_punishment_cost = 0.0 if p.is_judge: for i in range(1, 13): setattr(p, f'p{i}_punish', 0) # 3. Every Round: Reshuffle PGG groups normal_players = [p for p in players if not p.is_judge] global_group_id = 1 for sg in sorted(set(p.subgroup for p in normal_players)): sg_players = [p for p in normal_players if p.subgroup == sg] random.shuffle(sg_players) for i in range(0, len(sg_players), 4): for p in sg_players[i:i+4]: p.pgg_group = global_group_id global_group_id += 1 class Instructions(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 return True @staticmethod def vars_for_template(player: Player): return dict( treatment=player.session.config.get('treatment_type'), is_judge=player.is_judge, endowment=C.ENDOWMENT, mpcr=C.MPCR, ) class NormsPersonal(Page): form_model = 'player' form_fields = ['np_0', 'np_5', 'np_10', 'np_15', 'np_20'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 class NormsNormative(Page): form_model = 'player' form_fields = ['nn_0', 'nn_5', 'nn_10', 'nn_15', 'nn_20'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 class NormsEmpirical(Page): form_model = 'player' form_fields = ['expected_contribution'] @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Cooperation(Page): form_model = 'player' form_fields = ['contribution'] @staticmethod def is_displayed(player: Player): return not player.is_judge @staticmethod def vars_for_template(player: Player): return dict(endowment=C.ENDOWMENT, mpcr=C.MPCR) class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): players = group.subsession.get_players() normal_players = [p for p in players if not p.is_judge] # 1. Calculate PGG outcomes pgg_ids = set(p.pgg_group for p in normal_players) for g_id in pgg_ids: members = [p for p in normal_players if p.pgg_group == g_id] total_contrib = sum(m.contribution for m in members) for m in members: m.payoff_pre = float(C.ENDOWMENT - m.contribution + (C.MPCR * total_contrib)) m.payoff = math.ceil(m.payoff_pre) # 2. Calculate Judge Payoff (Average of Pre-Punishment Payoffs) judges = [p for p in players if p.is_judge] for j in judges: sg_players = [p for p in normal_players if p.subgroup == j.subgroup] if sg_players: avg_payoff = float(sum(p.payoff for p in sg_players) / len(sg_players)) j.payoff = math.ceil(avg_payoff) else: j.payoff = 0 class JudgeWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): subsession = group.subsession treatment = subsession.session.config.get('treatment_type') if treatment == 'no_judge': return # LOGIC FOR AI_judge (No human judges exist) if treatment == 'AI_judge': normal_players = subsession.get_players() # All players are normal subgroups = set(p.subgroup for p in normal_players) for sg in subgroups: sg_players = [p for p in normal_players if p.subgroup == sg] if not sg_players: continue avg_contrib = sum(p.contribution for p in sg_players) / len(sg_players) for p in sg_players: deviation = avg_contrib - p.contribution # Formula: min(5, max(0, round((avg - contrib) / 2))) points = min(5, max(0, round(deviation / 2))) p.punishment_received = points return # LOGIC FOR humanAI_judge (Pre-fill human judges) if treatment == 'humanAI_judge': judges = [p for p in subsession.get_players() if p.is_judge] for judge in judges: sg_players = [p for p in subsession.get_players() if not p.is_judge and p.subgroup == judge.subgroup] if not sg_players: continue avg_contrib = sum(p.contribution for p in sg_players) / len(sg_players) for i, p in enumerate(sg_players): deviation = avg_contrib - p.contribution points = min(5, max(0, round(deviation / 2))) setattr(judge, f'p{i+1}_punish', points) class Judge(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): treatment = player.session.config.get('treatment_type') return player.is_judge and treatment in ['human_judge', 'humanAI_judge'] @staticmethod def get_form_fields(player: Player): sg_players = [p for p in player.subsession.get_players() if not p.is_judge and p.subgroup == player.subgroup] return [f'p{i+1}_punish' for i in range(len(sg_players))] @staticmethod def vars_for_template(player: Player): sg_players = [p for p in player.subsession.get_players() if not p.is_judge and p.subgroup == player.subgroup] group_items = [] for i, p in enumerate(sg_players): group_items.append({'player': p, 'field': f'p{i+1}_punish'}) pgg_groups = {} for item in group_items: gid = item['player'].pgg_group pgg_groups.setdefault(gid, []).append(item) return dict(pgg_groups=pgg_groups) @staticmethod def before_next_page(player: Player, timeout_happened): sg_players = [p for p in player.subsession.get_players() if not p.is_judge and p.subgroup == player.subgroup] for i, p in enumerate(sg_players): val = getattr(player, f'p{i+1}_punish') p.punishment_received = val class FinalWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): players = group.subsession.get_players() normal_players = [p for p in players if not p.is_judge] # Apply Punishments pgg_ids = set(p.pgg_group for p in normal_players) for g_id in pgg_ids: members = [p for p in normal_players if p.pgg_group == g_id] total_punish = sum(m.punishment_received for m in members) shared_cost = total_punish / 4.0 for m in members: m.group_punishment_cost = shared_cost final_val = float(m.payoff_pre - m.punishment_received - shared_cost) m.payoff = math.ceil(final_val) class Results(Page): @staticmethod def vars_for_template(player: Player): if player.is_judge: return dict() group_members = [p for p in player.subsession.get_players() if not p.is_judge and p.pgg_group == player.pgg_group] anon_members = [] for i, m in enumerate(group_members): anon_members.append({ 'label': chr(65+i), 'contribution': m.contribution, 'punishment': m.punishment_received }) total_punish = sum(m.punishment_received for m in group_members) total_contribution = sum(m.contribution for m in group_members) return dict( anon_members=anon_members, total_group_punish=total_punish, total_contribution=total_contribution ) class NormsPersonalPost(Page): form_model = 'player' form_fields = ['np_0_post', 'np_5_post', 'np_10_post', 'np_15_post', 'np_20_post'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS class NormsNormativePost(Page): form_model = 'player' form_fields = ['nn_0_post', 'nn_5_post', 'nn_10_post', 'nn_15_post', 'nn_20_post'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS class NormsEmpiricalPost(Page): form_model = 'player' form_fields = ['expected_contribution_post'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS class NormsAndPaymentWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): subsession = group.subsession session = subsession.session if subsession.round_number != C.NUM_ROUNDS: return players_round1 = subsession.in_round(1).get_players() players_roundN = subsession.in_round(C.NUM_ROUNDS).get_players() # 1. Choose random levels levels = [0, 5, 10, 15, 20] pre_level = random.choice(levels) post_level = random.choice(levels) # 2. Calculate Averages (Personal Norms) pre_vals = [getattr(p, f'np_{pre_level}') for p in players_round1] avg_pre_personal = sum(pre_vals) / len(pre_vals) if pre_vals else 0 post_vals = [getattr(p, f'np_{post_level}_post') for p in players_roundN] avg_post_personal = sum(post_vals) / len(post_vals) if post_vals else 0 # 3. Calculate Average Session Contribution all_subsessions = session.get_subsessions() all_normal_players = [] for ss in all_subsessions: all_normal_players.extend([p for p in ss.get_players() if not p.is_judge]) total_contrib = sum(p.contribution for p in all_normal_players) avg_contrib = total_contrib / len(all_normal_players) if all_normal_players else 0 # 4. Assign bonuses to PLAYERS (Save to DB) for pN in players_roundN: p1 = pN.in_round(1) # Store common session info pN.chosen_level_pre = pre_level pN.chosen_level_post = post_level pN.avg_personal_norm_pre = avg_pre_personal pN.avg_personal_norm_post = avg_post_personal pN.avg_contribution_session = avg_contrib # Bonuses if abs(getattr(p1, f'nn_{pre_level}') - avg_pre_personal) <= 3: pN.bonus_norm_pre = 50 if abs(p1.expected_contribution - avg_contrib) <= 3: pN.bonus_emp_pre = 50 if abs(getattr(pN, f'nn_{post_level}_post') - avg_post_personal) <= 3: pN.bonus_norm_post = 50 if abs(pN.expected_contribution_post - avg_contrib) <= 3: pN.bonus_emp_post = 50 # 5. PGG Payment: Random 3 Rounds all_round_nums = list(range(1, C.NUM_ROUNDS + 1)) for pN in players_roundN: if C.NUM_ROUNDS >= 3: selected_rounds = random.sample(all_round_nums, 3) else: selected_rounds = all_round_nums selected_rounds.sort() # Calculate PGG Sum pgg_sum = 0 for r_num in selected_rounds: pr = pN.in_round(r_num) pgg_sum += pr.payoff # Calculate CZK (Multiplier = 3) pgg_czk = pgg_sum * 3 # Total Norms Bonus norms_sum = (pN.bonus_norm_pre + pN.bonus_emp_pre + pN.bonus_norm_post + pN.bonus_emp_post) # --- CORRECTION IS HERE --- # Sum for this part total_this_part = pgg_czk + norms_sum # Save to the new field pN.pgg_part_total_czk = total_this_part # ALSO save to the old field so the Results Summary page displays it correctly pN.final_payment_czk = total_this_part # Save basic stats pN.paid_rounds_str = str(selected_rounds) pN.pgg_payoff_sum = pgg_sum pN.pgg_czk_total = pgg_czk pN.norms_total_bonus = norms_sum class Part1CalculationWaitPage(WaitPage): wait_for_all_groups = True # Global matching requires everyone @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def after_all_players_arrive(subsession: Subsession): # Double check purely for safety if subsession.round_number != C.NUM_ROUNDS: return # These are the players in the CURRENT round (e.g., Round 3) all_players = subsession.get_players() # ========================== # 1. PD PAYMENT (Real Logic) # ========================== pd_players = list(all_players) random.shuffle(pd_players) if len(pd_players) % 2 == 1: leftover = pd_players.pop() leftover.participant.vars['pd_payoff'] = 0 leftover.participant.vars['pd_role'] = 'Leftover (Odd number)' pd_pairs = [] while len(pd_players) >= 2: pd_pairs.append((pd_players.pop(), pd_players.pop())) PD_MATRIX = { (True, True): 750, (True, False): 500, (False, True): 1000, (False, False): 600 } for p1, p2 in pd_pairs: # 1. Assign Roles using current round objects if random.choice([True, False]): mover_curr, responder_curr = p1, p2 else: mover_curr, responder_curr = p2, p1 # 2. Access Data from ROUND 1 objects (where the data is stored) mover_r1 = mover_curr.in_round(1) responder_r1 = responder_curr.in_round(1) # 3. Get Actions action_mover = mover_r1.p1_cooperate_PD # Use mover's action to determine which conditional choice to check if action_mover: action_responder = responder_r1.p1_cooperate_Cond_PD_C else: action_responder = responder_r1.p1_cooperate_Cond_PD_D # 4. Calculate pay_mover = PD_MATRIX.get((action_mover, action_responder), 0) pay_responder = PD_MATRIX.get((action_responder, action_mover), 0) # 5. Save to participant vars (shared across rounds, so using current player is fine) mover_curr.participant.vars['pd_payoff'] = pay_mover mover_curr.participant.vars['pd_role'] = 'Mover (Unconditional decicion)' responder_curr.participant.vars['pd_payoff'] = pay_responder responder_curr.participant.vars['pd_role'] = 'Responder (Conditional decision)' # ============================= # 2. EPPER PAYMENT (Real Logic) # ============================= epper_players = list(all_players) random.shuffle(epper_players) midpoint = len(epper_players) // 2 group_self = epper_players[:midpoint] group_other = epper_players[midpoint:] # Group Self: Get their OWN bonus available_other_payoffs = [] for p in group_self: # Retrieve data from Round 1 p_r1 = p.in_round(1) p.participant.vars['epper_final'] = p_r1.epper_bonus p.participant.vars['epper_role'] = 'Paid Your Choice' # Add the amount they generated for others to the pool available_other_payoffs.append(p_r1.epper_bonus_other) # Group Other: Get random OTHER bonus random.shuffle(available_other_payoffs) for i, p in enumerate(group_other): if i < len(available_other_payoffs): val = available_other_payoffs[i] else: val = 0 p.participant.vars['epper_final'] = val p.participant.vars['epper_role'] = 'Paid Other person Choice' # ========================== # 3. BRET PAYMENT & FINAL SUMMATION # ========================== for p in all_players: # 1. Get BRET Outcome p_r1 = p.in_round(1) if p_r1.p1_bomb: p.participant.vars['bret_payoff'] = 0 else: p.participant.vars['bret_payoff'] = (p_r1.p1_boxes_collected or 0) * 30 # 2. Retrieve all Part 1 Points (Stored in participant vars earlier in this function) pd_points = p.participant.vars.get('pd_payoff', 0) epper_points = p.participant.vars.get('epper_final', 0) bret_points = p.participant.vars.get('bret_payoff', 0) # 3. Sum and Convert Part 1 total_points = pd_points + epper_points + bret_points part1_czk_value = math.ceil(total_points / 30) # 4. Save Part 1 stats to DB p.part1_points_total = total_points p.part1_czk = part1_czk_value # 5. CALCULATE GRAND TOTAL (PGG Part + Part 1 Part) # pgg_part_total_czk was calculated in the previous WaitPage p.grand_total_czk = p.pgg_part_total_czk + part1_czk_value # 6. Final OTree Payoff (for Admin interface) p.participant.payoff = p.grand_total_czk class ResultsSummary(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): p1 = player.in_round(1) # 1. Helper to force numbers (removes "points" label) def to_num(curr): return int(curr) if curr else 0 # 2. Extract Round Details # We need to parse the string "[1, 5, 8]" back into a list to get specific payoffs import ast try: selected_rounds = ast.literal_eval(player.paid_rounds_str) except: selected_rounds = [] round_details = [] for r_num in selected_rounds: # Access the player object from that specific round p_past = player.in_round(r_num) # Append dict to list round_details.append({ 'round': r_num, 'payoff': to_num(p_past.payoff) }) # 3. Return full dictionary return dict( # --- New: List of round details --- round_payment_details=round_details, # --- Pre/Post Data --- norm_pre_level=player.chosen_level_pre, avg_pre_personal=round(player.avg_personal_norm_pre, 2), my_nn_pre=getattr(p1, f'nn_{player.chosen_level_pre}'), # Bonuses (Converted) bonus_norm_pre=to_num(player.bonus_norm_pre), my_emp_pre=p1.expected_contribution, bonus_emp_pre=to_num(player.bonus_emp_pre), norm_post_level=player.chosen_level_post, avg_post_personal=round(player.avg_personal_norm_post, 2), my_nn_post=getattr(player, f'nn_{player.chosen_level_post}_post'), bonus_norm_post=to_num(player.bonus_norm_post), my_emp_post=player.expected_contribution_post, bonus_emp_post=to_num(player.bonus_emp_post), avg_contribution_session=round(player.avg_contribution_session, 2), # --- PGG Info (Converted) --- paid_rounds_str=player.paid_rounds_str, pgg_payoff_sum=to_num(player.pgg_payoff_sum), pgg_czk_total=to_num(player.pgg_czk_total), # --- Totals (Converted) --- norms_total_bonus=to_num(player.norms_total_bonus), final_payment_czk=to_num(player.final_payment_czk) ) class Part1Summary(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): # 1. Get raw loaded data p1_data = player.participant.vars.get('p1_data', {}) vars = player.participant.vars # Helper to force number (removes "points" label from Currency objects) def to_num(val): return int(val) if val is not None else 0 return { 'p1_id': vars.get('p1_id', 'Unknown'), # Raw Data Display 'cooperate_PD': p1_data.get('cooperate_PD'), 'cooperate_Cond_PD_C': p1_data.get('cooperate_Cond_PD_C'), 'cooperate_Cond_PD_D': p1_data.get('cooperate_Cond_PD_D'), 'boxes_collected': p1_data.get('boxes_collected'), 'bomb': p1_data.get('bomb'), 'epper_scenario': p1_data.get('epper_scenario_selected'), 'epper_choice': p1_data.get('epper_choice_raw'), 'epper_payoff_me': p1_data.get('epper_payoff_me'), 'epper_payoff_other': p1_data.get('epper_payoff_other'), 'finished': p1_data.get('finished_part_1'), # Roles & Payoffs (Calculated) 'pd_role': vars.get('pd_role', 'Not calculated'), 'pd_payoff': vars.get('pd_payoff', 0), 'epper_role': vars.get('epper_role', 'Not calculated'), 'epper_final': vars.get('epper_final', 0), 'bret_payoff': vars.get('bret_payoff', 0), # Totals - Converted to plain integers for clean display 'part1_points_total': to_num(player.part1_points_total), 'part1_czk': to_num(player.part1_czk), } class Demographics(Page): form_model = 'player' form_fields = ['age', 'gender', 'field_of_study'] @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS class FinalPayment(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): return dict( pgg_part_czk=int(player.pgg_part_total_czk), part1_czk=int(player.part1_czk), grand_total=int(player.grand_total_czk) ) page_sequence = [ IDInput, Intro, SetupWaitPage, Instructions, NormsPersonal, NormsNormative, NormsEmpirical, Cooperation, ResultsWaitPage, JudgeWaitPage, Judge, FinalWaitPage, Results, NormsPersonalPost, NormsNormativePost, NormsEmpiricalPost, NormsAndPaymentWaitPage, Part1CalculationWaitPage, ResultsSummary, Part1Summary, Demographics, FinalPayment ]