from otree.api import * from .models import Constants, Player import json # ----------------------------- # 1. Reveal Page # ----------------------------- class Reveal(Page): form_model = 'player' form_fields = [ 'selected_option', 'revisit_cost', 'revealed_fields', 'num_revealed_fields', 'revisited_trainings', 'refresh_penalty', ] def get_period_info(self, trial_idx): """Helper to determine period and relative round.""" if 1 <= trial_idx <= 6: return 1, trial_idx if 7 <= trial_idx <= 12: return 2, trial_idx - 6 if 13 <= trial_idx <= 18: return 3, trial_idx - 12 if 19 <= trial_idx <= 24: return 4, trial_idx - 18 if 25 <= trial_idx <= 30: return 5, trial_idx - 24 if 31 <= trial_idx <= 36: return 6, trial_idx - 30 return 0, 0 def vars_for_template(self): p = self.player.participant r = p.vars['current_trial_index'] period_num, round_in_period = self.get_period_info(r) # Pull trial and period-specific weight data trial_options = p.vars.get('TRIALS', {}).get(r, []) all_weights = p.vars.get('WEIGHTS', {}) current_weights = all_weights.get(period_num, {}) reveal_cost = current_weights.get('RevealCost', 0) options = [] for row in trial_options: options.append({ 'name': f"Project {row['Opt']}", 'Opt': row['Opt'], 'fields': [row['A1'], row['A2'], row['A3']], 'ev_weights': [ current_weights.get('A1', 0), current_weights.get('A2', 0), current_weights.get('A3', 0) ], 'true_value': row['true_value'] }) training_done_status = { 1: p.vars.get('training_1_done', False), 2: p.vars.get('training_2_done', False), 3: p.vars.get('training_3_done', False) } # Hardcoded training data for revisits (Static) training_data = { 1: {'data': [{'Opt': 1, 'A1': 3, 'A2': 5, 'A3': 4}, {'Opt': 2, 'A1': 6, 'A2': 4, 'A3': 1}, {'Opt': 3, 'A1': 8, 'A2': 2, 'A3': 2}, {'Opt': 4, 'A1': 1, 'A2': 7, 'A3': 5}], 'weights': {'A1': 25, 'A2': 50, 'A3': 25}, 'correct_answer': 'A2'}, 2: {'data': [{'Opt': 1, 'A1': 5, 'A2': 5, 'A3': 9}, {'Opt': 2, 'A1': 4, 'A2': 4, 'A3': 8}, {'Opt': 3, 'A1': 7, 'A2': 2, 'A3': 11}, {'Opt': 4, 'A1': 1, 'A2': 7, 'A3': 5}], 'weights': {'A1': 30, 'A2': 50, 'A3': 20}, 'correct_answer': 'A1+4'}, 3: {'data': [{'Opt': 1, 'A1': 3, 'A2': 2, 'A3': 1}, {'Opt': 2, 'A1': 6, 'A2': 5, 'A3': 2}, {'Opt': 3, 'A1': 9, 'A2': 9, 'A3': 3}, {'Opt': 4, 'A1': 12, 'A2': 8, 'A3': 4}], 'weights': {'A1': 10, 'A2': 40, 'A3': 50}, 'correct_answer': 'opt1'} } return dict( trial_num=r, period_num=period_num, round_in_period=round_in_period, options=options, reveal_cost=reveal_cost, training_done_status=training_done_status, training_data=training_data, player_code=self.player.participant.code ) def before_next_page(self): player = self.player p = player.participant r = p.vars['current_trial_index'] period_num, _ = self.get_period_info(r) selected = player.selected_option revisit_cost = player.revisit_cost or 0.0 refresh_penalty = player.refresh_penalty or 0.0 trial_options = p.vars['TRIALS'].get(r, []) all_weights = p.vars.get('WEIGHTS', {}) current_weights = all_weights.get(period_num, {}) # --- Value of Choice --- value_of_choice = 0 if selected is not None: for row in trial_options: if row['Opt'] == selected: value_of_choice = row['true_value'] break player.value_of_choice = value_of_choice # --- Reveal Cost (Period Specific) --- reveal_cost_per_field = current_weights.get('RevealCost', 0) num_revealed = player.num_revealed_fields or 0 total_reveal_cost = num_revealed * reveal_cost_per_field player.reveal_cost_total = total_reveal_cost # --- Net Performance --- total_costs_with_penalty = total_reveal_cost + revisit_cost + refresh_penalty player.trial_net_performance = float(value_of_choice - total_costs_with_penalty) # Revisit Logic COST_PER_REVISIT = 20.0 num_revisits = round(revisit_cost / COST_PER_REVISIT) if revisit_cost > 0 else 0 if 'review_data' not in p.vars: p.vars['review_data'] = {} p.vars['review_data'][r] = { 'value': player.value_of_choice or 0.0, 'reveals': player.num_revealed_fields or 0, 'reveal_cost': total_reveal_cost, 'revisit_cost': revisit_cost, 'net_perf': player.trial_net_performance, 'total_cost': total_costs_with_penalty, 'num_revisits': num_revisits, 'refresh_penalty': refresh_penalty, } if 'trial_net_performances' not in p.vars: p.vars['trial_net_performances'] = {} p.vars['trial_net_performances'][r] = player.trial_net_performance player.visited_training = None p.vars['current_trial_index'] += 1 def is_displayed(self): p = self.player.participant r = self.round_number # 1. Block Reveal on all non-working rounds defined in Constants if r in Constants.OFFER_ROUNDS or r in Constants.REVIEW_ROUNDS or r in Constants.TRAINING_ROUNDS: return False # 2. Block Reveal on explicit transition/buffer rounds if r in [8, 18, 28, 38, 46, 54, 55]: return False # 3. Block Reveal if the EmptyPage trigger is active if p.vars.get('trigger_empty_page_in_round') == r: return False # 4. Correct the "Slot" logic: # You must block Reveal if the round falls within the transition window # Period 2 transition: 15, 16, 17, 18 # Period 3 transition: 25, 26, 27, 28 # Period 4 transition: 35, 36, 37, 38 if 15 <= r <= 18: return False if 25 <= r <= 28: return False if 35 <= r <= 38: return False # 5. Stop if all 36 trials are completed if p.vars.get('current_trial_index', 1) > 36: return False return True # ----------------------------- # TrainingOffer Page # ----------------------------- class TrainingOffer(Page): form_model = 'player' form_fields = ['wants_training'] def is_displayed(self): return self.round_number in Constants.OFFER_ROUNDS def vars_for_template(self): # Maps the specific round to the correct training number mapping = {16: 1, 26: 2, 36: 3} return dict(offer_training_num=mapping.get(self.round_number, 0)) def before_next_page(self): p = self.player.participant # Ensure round mapping matches OFFER_ROUNDS round_to_training = {16: 1, 26: 2, 36: 3}.get(self.round_number, 0) if round_to_training: p.vars[f'training_{round_to_training}_taken'] = self.player.wants_training # ----------------------------- # TrainingGate Page # ----------------------------- class TrainingGate(Page): def get_lowest_open_training_num(self, p): for i in range(1, 4): if not p.vars.get(f'training_{i}_done', False): return i return 0 def get_slot_for_round(self, r): # Updated to match your logic: 17->1, 27->2, 37->3 if r == 17: return 1 if r == 27: return 2 if r == 37: return 3 return 0 def is_displayed(self): p = self.player.participant r = self.round_number if r not in Constants.TRAINING_ROUNDS: return False slot = self.get_slot_for_round(r) # Verify they accepted it in the Offer round if not p.vars.get(f'training_{slot}_taken', False): return False return True def vars_for_template(self): training_num = self.get_lowest_open_training_num(self.player.participant) # hardcoded trainingsdata training_data = { 1: {'options': [{'name': 'Project 1', 'fields': [3, 5, 4], 'ev_weights': [25, 50, 25]}, {'name': 'Project 2', 'fields': [6, 4, 1], 'ev_weights': [25, 50, 25]}, {'name': 'Project 3', 'fields': [8, 2, 2], 'ev_weights': [25, 50, 25]}, {'name': 'Project 4', 'fields': [1, 7, 5], 'ev_weights': [25, 50, 25]}], 'correct_answer': 'A2'}, 2: {'options': [{'name': 'Project 1', 'fields': [5, 5, 9], 'ev_weights': [30, 50, 20]}, {'name': 'Project 2', 'fields': [4, 4, 8], 'ev_weights': [30, 50, 20]}, {'name': 'Project 3', 'fields': [7, 2, 11], 'ev_weights': [30, 50, 20]}, {'name': 'Project 4', 'fields': [1, 7, 5], 'ev_weights': [30, 50, 20]}], 'correct_answer': 'A1+4'}, 3: {'options': [{'name': 'Project 1', 'fields': [3, 2, 1], 'ev_weights': [10, 40, 50]}, {'name': 'Project 2', 'fields': [6, 5, 2], 'ev_weights': [10, 40, 50]}, {'name': 'Project 3', 'fields': [9, 9, 3], 'ev_weights': [10, 40, 50]}, {'name': 'Project 4', 'fields': [12, 8, 4], 'ev_weights': [10, 40, 50]}], 'correct_answer': 'opt1'} } options = training_data.get(training_num, {}).get('options', []) correct_answer = training_data.get(training_num, {}).get('correct_answer', "") return dict( training_num=training_num, options=options, correct_answer=correct_answer, Constants=Constants ) def before_next_page(self): p = self.player.participant training_num = self.get_lowest_open_training_num(p) if training_num > 0: p.vars[f'training_{training_num}_done'] = True p.vars['current_trial_index'] += 0 p.vars['trigger_empty_page_in_round'] = self.round_number + 1 player = self.player player.selected_option = None player.revealed_fields = json.dumps([]) player.num_revealed_fields = 0 player.trial_net_performance = 0 player.visited_training = training_num player.training_accepted = player.wants_training # ----------------------------- # EmptyPage # ----------------------------- class EmptyPage(Page): timeout_seconds = 3 def is_displayed(self): r = self.round_number p_vars = self.player.participant.vars # Show if it's a scheduled empty round OR the dynamic trigger is set return r in [8, 18, 28, 38, 46, 54] or p_vars.get('trigger_empty_page_in_round') == r def before_next_page(self): # Reset the trigger self.player.participant.vars['trigger_empty_page_in_round'] = 0 # ----------------------------- # PerformanceReview # ----------------------------- class PerformanceReview(Page): def get_period_num(self, round_num): # Updated mapping to match your specified rounds mapping = {7: 1, 15: 2, 25: 3, 35: 4, 45: 5, 53: 6} return mapping.get(round_num, 0) def get_trial_range_for_period(self, period_num): ranges = { 1: range(1, 7), 2: range(7, 13), 3: range(13, 19), 4: range(19, 25), 5: range(25, 31), 6: range(31, 37) } return ranges.get(period_num, range(0)) def calculate_period_performance(self, player): p = player.participant period_num = self.get_period_num(self.round_number) trial_range = self.get_trial_range_for_period(period_num) # round sum and transform to string total_perf = sum(p.vars.get('trial_net_performances', {}).get(i, 0) for i in trial_range) return round(total_perf, 2) def is_displayed(self): return self.round_number in Constants.REVIEW_ROUNDS def vars_for_template(self): period_num = self.get_period_num(self.round_number) period_net_perf = self.calculate_period_performance(self.player) period_trials = self.get_trial_range_for_period(period_num) # Das ist z.B. range(7, 13) p_vars = self.player.participant.vars review_data = p_vars.get('review_data', {}) # Wir bestimmen den ersten Trial dieser Periode, um davon abzuziehen # Falls period_trials leer ist, nehmen wir 0 start_trial_of_period = min(period_trials) if period_trials else 0 show_refresh_penalty_column = False table_rows = [] for trial_index in period_trials: data = review_data.get(trial_index) if data and data.get('value') is not None: penalty_value = data.get('refresh_penalty', 0.0) if float(penalty_value) > 0: show_refresh_penalty_column = True # HIER BERECHNEN WIR DIE RELATIVE RUNDE: # Beispiel: Trial 7 (Index) - Start 7 + 1 = Runde 1 relative_round_num = trial_index - start_trial_of_period + 1 table_rows.append({ 'round': relative_round_num, # Wir schicken die relative Nummer ans HTML 'value': data.get('value'), 'reveals': data.get('reveals'), 'num_revisits': data.get('num_revisits'), 'refresh_penalty': penalty_value, 'net_perf': data.get('net_perf'), }) return dict( period_num=period_num, period_net_perf=period_net_perf, table_rows=table_rows, show_refresh_penalty_column=show_refresh_penalty_column, ) # ----------------------------- # FinalReview # ----------------------------- class FinalReview(Page): def calculate_period_performance(self, player, period_num): mapping = {1: range(1, 7), 2: range(7, 13), 3: range(13, 19), 4: range(19, 25), 5: range(25, 31), 6: range(31,37)} trial_range = mapping.get(period_num, range(0)) p = player.participant total_perf = sum(p.vars.get('trial_net_performances', {}).get(i, 0) for i in trial_range) return round(total_perf, 2) def vars_for_template(self): player = self.player # collect net performance for all periods period_data = [] for i in range(1, 7): # Periods 1 to 5 net_perf = self.calculate_period_performance(player, i) period_data.append({ 'period_num': i, 'net_perf': f"{net_perf:.2f}" # round to 2 decimals }) total_score = sum( self.calculate_period_performance(player, i) for i in range(1, 7) ) return dict( period_data=period_data, total_score=f"{total_score:.2f}", ) def is_displayed(self): # show FinalReview only in the second to last round (before Thankyou page) return self.round_number == Constants.num_rounds - 1 # ----------------------------- # ThankYou # ----------------------------- class ThankYou(Page): def is_displayed(self): return self.round_number == Constants.num_rounds page_sequence = [ PerformanceReview, TrainingOffer, TrainingGate, EmptyPage, Reveal, FinalReview, ]