from otree.api import * import random import json import itertools import time from random import choice doc = """ """ class C(BaseConstants): NAME_IN_URL = 'competition' PLAYERS_PER_GROUP = 4 NUM_ROUNDS = 1 TASK_TIME_SECONDS = 300 # 5 minutes NUM_PROBLEMS = 500 # large enough so they won't run out MIN_NUMBER = 10 MAX_NUMBER = 99 PIECE_RATE = 500 # Rupiah per correct answer ROUND2_TOURNAMENT_RATE = 2000 LOTTERY_SAFE = 1000 LOTTERY_BONUSES = [1500, 2000, 2500, 3000, 3500, 4000, 4500] RANK_GUESS_BONUS = 1000 SHOW_UP_FEE = 5000 # replace with your actual show-up fee class Subsession(BaseSubsession): pass def creating_session(subsession): pass class Group(BaseGroup): round2_winner_id = models.IntegerField(blank=True, null=True) class Player(BasePlayer): code = models.StringField( label="", ) num_correct = models.IntegerField(initial=0) num_attempted = models.IntegerField(initial=0) time_spent_seconds = models.FloatField(initial=0) # logs stored as strings/JSON problem_log = models.LongStringField(blank=True) answer_log = models.LongStringField(blank=True) result_log = models.LongStringField(blank=True) # hidden fields populated from JS during task hidden_num_correct = models.IntegerField(initial=0) hidden_num_attempted = models.IntegerField(initial=0) hidden_time_spent_seconds = models.FloatField(initial=0) hidden_problem_log = models.LongStringField(blank=True) hidden_answer_log = models.LongStringField(blank=True) hidden_result_log = models.LongStringField(blank=True) # ---------- Round 2 ---------- round2_num_correct = models.IntegerField(initial=0) round2_num_attempted = models.IntegerField(initial=0) round2_time_spent_seconds = models.FloatField(initial=0) round2_problem_log = models.LongStringField(blank=True) round2_answer_log = models.LongStringField(blank=True) round2_result_log = models.LongStringField(blank=True) round2_hidden_num_correct = models.IntegerField(initial=0) round2_hidden_num_attempted = models.IntegerField(initial=0) round2_hidden_time_spent_seconds = models.FloatField(initial=0) round2_hidden_problem_log = models.LongStringField(blank=True) round2_hidden_answer_log = models.LongStringField(blank=True) round2_hidden_result_log = models.LongStringField(blank=True) round2_is_winner = models.BooleanField(initial=False) round2_payment = models.IntegerField(initial=0) # Round 3 choice round3_scheme = models.StringField( choices=['piece_rate', 'tournament'], blank=True ) # Round 3 task outcomes round3_num_correct = models.IntegerField(initial=0) round3_num_attempted = models.IntegerField(initial=0) round3_time_spent_seconds = models.FloatField(initial=0) round3_problem_log = models.LongStringField(blank=True) round3_answer_log = models.LongStringField(blank=True) round3_result_log = models.LongStringField(blank=True) # hidden fields from HTML round3_hidden_num_correct = models.IntegerField(initial=0) round3_hidden_num_attempted = models.IntegerField(initial=0) round3_hidden_time_spent_seconds = models.FloatField(initial=0) round3_hidden_problem_log = models.LongStringField(blank=True) round3_hidden_answer_log = models.LongStringField(blank=True) round3_hidden_result_log = models.LongStringField(blank=True) # Round 3 payment outcome round3_is_winner = models.BooleanField(initial=False) round3_payment = models.IntegerField(initial=0) round4_scheme = models.StringField( choices=['piece_rate', 'tournament'], blank=True ) round4_is_winner = models.BooleanField(initial=False) round4_payment = models.IntegerField(initial=0) round1_rank_guess = models.IntegerField( min=1, max=4, blank=True ) round2_rank_guess = models.IntegerField( min=1, max=4, blank=True ) # ---------- Lottery choices ---------- lottery_q1 = models.StringField(choices=['safe', 'risky']) lottery_q2 = models.StringField(choices=['safe', 'risky']) lottery_q3 = models.StringField(choices=['safe', 'risky']) lottery_q4 = models.StringField(choices=['safe', 'risky']) lottery_q5 = models.StringField(choices=['safe', 'risky']) lottery_q6 = models.StringField(choices=['safe', 'risky']) lottery_q7 = models.StringField(choices=['safe', 'risky']) lottery_selected_question = models.IntegerField(blank=True, null=True) lottery_selected_choice = models.StringField(blank=True) lottery_draw_won = models.BooleanField(blank=True, null=True) lottery_selected_bonus = models.IntegerField(blank=True, null=True) lottery_payment = models.IntegerField(initial=0) # ---------- Rank guess payment ---------- round1_actual_rank = models.IntegerField(blank=True, null=True) round2_actual_rank = models.IntegerField(blank=True, null=True) round1_guess_correct = models.BooleanField(initial=False) round2_guess_correct = models.BooleanField(initial=False) rank_guess_payment = models.IntegerField(initial=0) # ---------- Final randomly selected round ---------- selected_payment_round = models.IntegerField(blank=True, null=True) selected_payment_label = models.StringField(blank=True) selected_round_payment = models.IntegerField(initial=0) # ---------- Final payoff summary ---------- final_total_payment = models.IntegerField(initial=0) # ---------- Questionnaire ---------- gender = models.StringField( choices=[ ['male', 'Laki-laki'], ['female', 'Perempuan'], ], widget=widgets.RadioSelect, blank=True, ) age_year = models.IntegerField(min=0, max=100, blank=True) age_month = models.IntegerField(min=0, max=11, blank=True) desire_university = models.IntegerField( choices=list(range(0, 11)), widget=widgets.RadioSelectHorizontal, blank=True, ) preferred_major = models.StringField(blank=True) preferred_university = models.StringField(blank=True) snbp_applied = models.StringField( choices=[ ['yes', 'Ya'], ['no', 'Tidak'], ], widget=widgets.RadioSelect, blank=True, ) snbp_choice1 = models.StringField(blank=True) snbp_choice2 = models.StringField(blank=True) snbp_choice3 = models.StringField(blank=True) snbp_accepted_program = models.StringField(blank=True) snbt_plan = models.StringField( choices=[ ['yes', 'Ya'], ['no', 'Tidak'], ['not_sure', 'Ragu'], ], widget=widgets.RadioSelect, blank=True, ) snbt_choice1 = models.StringField(blank=True) snbt_choice2 = models.StringField(blank=True) snbt_choice3 = models.StringField(blank=True) path_snbp = models.BooleanField(blank=True) path_snbt = models.BooleanField(blank=True) path_independent = models.BooleanField(blank=True) path_service = models.BooleanField(blank=True) path_private = models.BooleanField(blank=True) path_other = models.BooleanField(blank=True) path_other_text = models.StringField(blank=True) family_support = models.IntegerField( choices=[ [1, 'Sangat rendah'], [2, 'Rendah'], [3, 'Sedang'], [4, 'Tinggi'], [5, 'Sangat tinggi'], ], widget=widgets.RadioSelectHorizontal, blank=True, ) likelihood_pursue = models.IntegerField( choices=[ [1, 'Sangat kecil kemungkinannya'], [2, 'Kecil kemungkinannya'], [3, 'Netral'], [4, 'Besar kemungkinannya'], [5, 'Sangat besar kemungkinannya'], ], widget=widgets.RadioSelectHorizontal, blank=True, ) main_reason = models.LongStringField(blank=True) risk_preference = models.IntegerField( choices=list(range(0, 11)), widget=widgets.RadioSelectHorizontal, blank=True, ) income_importance = models.IntegerField( choices=[ [1, 'Sangat penting'], [2, 'Penting'], [3, 'Biasa saja'], [4, 'Tidak penting'], [5, 'Sangat tidak penting'], ], widget=widgets.RadioSelectHorizontal, blank=True, ) instruction_difficulty = models.IntegerField( choices=[ [1, 'Sangat sulit'], [2, 'Sulit'], [3, 'Netral'], [4, 'Mudah'], [5, 'Sangat mudah'], ], widget=widgets.RadioSelectHorizontal, blank=True, ) other_comments = models.LongStringField(blank=True) def generate_problem(): numbers = [random.randint(C.MIN_NUMBER, C.MAX_NUMBER) for _ in range(5)] total = sum(numbers) return { 'numbers': numbers, 'solution': total, } def generate_problem_set(): return [generate_problem() for _ in range(C.NUM_PROBLEMS)] def compute_unique_ranks(players, score_attr_name): """ Assign unique ranks 1..N using random tie-breaking. Higher score = better rank. """ shuffled = players[:] random.shuffle(shuffled) sorted_players = sorted( shuffled, key=lambda p: getattr(p, score_attr_name), reverse=True ) rank_map = {} for idx, p in enumerate(sorted_players, start=1): rank_map[p.id_in_group] = idx return rank_map def compute_round4_outcomes(group: Group): players = group.get_players() round1_scores = {p.id_in_group: p.num_correct for p in players} max_round1 = max(round1_scores.values()) top_players = [p for p in players if p.num_correct == max_round1] tournament_winner = random.choice(top_players) for p in players: if p.round4_scheme == 'piece_rate': p.round4_is_winner = False p.round4_payment = C.PIECE_RATE * p.num_correct elif p.round4_scheme == 'tournament': if p.id_in_group == tournament_winner.id_in_group: p.round4_is_winner = True p.round4_payment = C.ROUND2_TOURNAMENT_RATE * p.num_correct else: p.round4_is_winner = False p.round4_payment = 0 def compute_rank_guess_payments(group: Group): players = group.get_players() round1_ranks = compute_unique_ranks(players, 'num_correct') round2_ranks = compute_unique_ranks(players, 'round2_num_correct') for p in players: p.round1_actual_rank = round1_ranks[p.id_in_group] p.round2_actual_rank = round2_ranks[p.id_in_group] p.round1_guess_correct = (p.round1_rank_guess == p.round1_actual_rank) p.round2_guess_correct = (p.round2_rank_guess == p.round2_actual_rank) num_correct_guesses = int(p.round1_guess_correct) + int(p.round2_guess_correct) p.rank_guess_payment = num_correct_guesses * C.RANK_GUESS_BONUS def compute_random_main_payment(group: Group): players = group.get_players() selected_round = random.randint(1, 4) for p in players: p.selected_payment_round = selected_round if selected_round == 1: p.selected_payment_label = 'Putaran 1: Bayaran per Jawaban Benar' p.selected_round_payment = C.PIECE_RATE * p.num_correct elif selected_round == 2: p.selected_payment_label = 'Putaran 2: Kompetisi' p.selected_round_payment = p.round2_payment elif selected_round == 3: if p.round3_scheme == 'piece_rate': p.selected_payment_label = 'Putaran 3: Bayaran per Jawaban Benar' else: p.selected_payment_label = 'Putaran 3: Kompetisi' p.selected_round_payment = p.round3_payment elif selected_round == 4: if p.round4_scheme == 'piece_rate': p.selected_payment_label = 'Putaran 4: Bayaran per Jawaban Benar' else: p.selected_payment_label = 'Putaran 4: Kompetisi' p.selected_round_payment = p.round4_payment def compute_final_total_payoff(group: Group): players = group.get_players() for p in players: p.final_total_payment = ( p.selected_round_payment + p.rank_guess_payment + p.lottery_payment + C.SHOW_UP_FEE ) p.payoff = p.final_total_payment class Welcome(Page): pass class Code(Page): form_model = 'player' form_fields = ['code'] class Wait1(WaitPage): wait_for_all_groups = True class Round1(Page): pass class Wait2(WaitPage): wait_for_all_groups = True class Round1Task(Page): form_model = 'player' form_fields = [ 'hidden_num_correct', 'hidden_num_attempted', 'hidden_time_spent_seconds', 'hidden_problem_log', 'hidden_answer_log', 'hidden_result_log', ] timeout_seconds = C.TASK_TIME_SECONDS @staticmethod def vars_for_template(player: Player): problems = generate_problem_set() return dict( piece_rate=C.PIECE_RATE, task_time_seconds=C.TASK_TIME_SECONDS, problems_json=json.dumps(problems), ) @staticmethod def before_next_page(player: Player, timeout_happened): player.num_correct = player.hidden_num_correct or 0 player.num_attempted = player.hidden_num_attempted or 0 player.time_spent_seconds = player.hidden_time_spent_seconds or 0 player.problem_log = player.hidden_problem_log or '' player.answer_log = player.hidden_answer_log or '' player.result_log = player.hidden_result_log or '' class Round1Result(Page): pass class Round2(Page): pass class Wait3(WaitPage): wait_for_all_groups = True class Round2Task(Page): form_model = 'player' form_fields = [ 'round2_hidden_num_correct', 'round2_hidden_num_attempted', 'round2_hidden_time_spent_seconds', 'round2_hidden_problem_log', 'round2_hidden_answer_log', 'round2_hidden_result_log', ] timeout_seconds = C.TASK_TIME_SECONDS @staticmethod def vars_for_template(player: Player): return dict( task_time_seconds=C.TASK_TIME_SECONDS, problems_json=json.dumps(generate_problem_set()), ) @staticmethod def before_next_page(player: Player, timeout_happened): player.round2_num_correct = player.round2_hidden_num_correct or 0 player.round2_num_attempted = player.round2_hidden_num_attempted or 0 player.round2_time_spent_seconds = player.round2_hidden_time_spent_seconds or 0 player.round2_problem_log = player.round2_hidden_problem_log or '' player.round2_answer_log = player.round2_hidden_answer_log or '' player.round2_result_log = player.round2_hidden_result_log or '' class Round2ResultsWaitPage(WaitPage): after_all_players_arrive = 'set_round2_winner' def set_round2_winner(group: Group): players = group.get_players() max_score = max(p.round2_num_correct for p in players) top_players = [p for p in players if p.round2_num_correct == max_score] winner = random.choice(top_players) group.round2_winner_id = winner.id_in_group for p in players: if p.id_in_group == winner.id_in_group: p.round2_is_winner = True p.round2_payment = p.round2_num_correct * C.ROUND2_TOURNAMENT_RATE else: p.round2_is_winner = False p.round2_payment = 0 class Round2Results(Page): @staticmethod def vars_for_template(player: Player): return dict( round2_num_correct=player.round2_num_correct, ) class Round3(Page): pass class Round3Choice(Page): form_model = 'player' form_fields = ['round3_scheme'] @staticmethod def error_message(player: Player, values): if not values.get('round3_scheme'): return 'Silakan pilih salah satu skema pembayaran.' class Wait4(WaitPage): wait_for_all_groups = True class Round3Task(Page): form_model = 'player' form_fields = [ 'round3_hidden_num_correct', 'round3_hidden_num_attempted', 'round3_hidden_time_spent_seconds', 'round3_hidden_problem_log', 'round3_hidden_answer_log', 'round3_hidden_result_log', ] timeout_seconds = C.TASK_TIME_SECONDS @staticmethod def vars_for_template(player: Player): if player.round3_scheme == 'piece_rate': chosen_scheme_text = 'Bayaran per Jawaban Benar' else: chosen_scheme_text = 'Kompetisi' return dict( task_time_seconds=C.TASK_TIME_SECONDS, problems_json=json.dumps(generate_problem_set()), chosen_scheme_text=chosen_scheme_text, ) @staticmethod def before_next_page(player: Player, timeout_happened): player.round3_num_correct = player.round3_hidden_num_correct or 0 player.round3_num_attempted = player.round3_hidden_num_attempted or 0 player.round3_time_spent_seconds = player.round3_hidden_time_spent_seconds or 0 player.round3_problem_log = player.round3_hidden_problem_log or '' player.round3_answer_log = player.round3_hidden_answer_log or '' player.round3_result_log = player.round3_hidden_result_log or '' if player.round3_scheme == 'piece_rate': player.round3_is_winner = False player.round3_payment = player.round3_num_correct * C.PIECE_RATE elif player.round3_scheme == 'tournament': others = [p for p in player.get_others_in_group()] other_round2_scores = [p.round2_num_correct for p in others] max_other_round2 = max(other_round2_scores) if player.round3_num_correct > max_other_round2: player.round3_is_winner = True player.round3_payment = player.round3_num_correct * C.ROUND2_TOURNAMENT_RATE elif player.round3_num_correct < max_other_round2: player.round3_is_winner = False player.round3_payment = 0 else: tied_opponents = [p for p in others if p.round2_num_correct == max_other_round2] lottery_pool_size = 1 + len(tied_opponents) win = random.randint(1, lottery_pool_size) == 1 player.round3_is_winner = win if win: player.round3_payment = player.round3_num_correct * C.ROUND2_TOURNAMENT_RATE else: player.round3_payment = 0 class Round3Results(Page): @staticmethod def vars_for_template(player: Player): if player.round3_scheme == 'piece_rate': chosen_scheme_text = 'Bayaran per Jawaban Benar' else: chosen_scheme_text = 'Kompetisi' return dict( chosen_scheme_text=chosen_scheme_text, ) class Round4(Page): pass class Round4Choice(Page): form_model = 'player' form_fields = ['round4_scheme'] @staticmethod def error_message(player, values): if not values.get('round4_scheme'): return 'Silakan pilih salah satu skema pembayaran.' class RankGuess(Page): form_model = 'player' form_fields = ['round1_rank_guess', 'round2_rank_guess'] @staticmethod def error_message(player, values): r1 = values.get('round1_rank_guess') r2 = values.get('round2_rank_guess') if r1 is None or r2 is None: return 'Silakan isi kedua jawaban.' if r1 < 1 or r1 > 4: return 'Jawaban untuk Putaran 1 harus berupa angka antara 1 dan 4.' if r2 < 1 or r2 > 4: return 'Jawaban untuk Putaran 2 harus berupa angka antara 1 dan 4.' class LotteryTask(Page): form_model = 'player' form_fields = [ 'lottery_q1', 'lottery_q2', 'lottery_q3', 'lottery_q4', 'lottery_q5', 'lottery_q6', 'lottery_q7', ] @staticmethod def vars_for_template(player: Player): rows = [] for i, bonus in enumerate(C.LOTTERY_BONUSES, start=1): rows.append(dict( qnum=i, safe=C.LOTTERY_SAFE, bonus=bonus, )) return dict(lottery_rows=rows) class LotteryWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): for p in group.get_players(): q = random.randint(1, 7) p.lottery_selected_question = q p.lottery_selected_bonus = C.LOTTERY_BONUSES[q - 1] selected_choice = getattr(p, f'lottery_q{q}') p.lottery_selected_choice = selected_choice if selected_choice == 'safe': p.lottery_draw_won = True p.lottery_payment = C.LOTTERY_SAFE else: won = random.choice([True, False]) p.lottery_draw_won = won p.lottery_payment = p.lottery_selected_bonus if won else 0 class LotteryOutcome(Page): @staticmethod def vars_for_template(player: Player): if player.lottery_selected_choice == 'safe': selected_option_text = f'Pasti (100%) mendapat {C.LOTTERY_SAFE} Rupiah' else: selected_option_text = ( f'Kemungkinan 50% mendapat 0 Rupiah dan kemungkinan 50% ' f'mendapat {player.lottery_selected_bonus} Rupiah' ) return dict( selected_option_text=selected_option_text, ) class FinalQuestionnaire(Page): form_model = 'player' form_fields = [ 'gender', 'age_year', 'age_month', 'desire_university', 'preferred_major', 'preferred_university', 'snbp_applied', 'snbp_choice1', 'snbp_choice2', 'snbp_choice3', 'snbp_accepted_program', 'snbt_plan', 'snbt_choice1', 'snbt_choice2', 'snbt_choice3', 'path_snbp', 'path_snbt', 'path_independent', 'path_service', 'path_private', 'path_other', 'path_other_text', 'family_support', 'likelihood_pursue', 'main_reason', 'risk_preference', 'income_importance', 'instruction_difficulty', 'other_comments', ] @staticmethod def error_message(player: Player, values): # Optional consistency checks only when respondent answered related question if values.get('path_other') and not (values.get('path_other_text') or '').strip(): return 'Jika Anda memilih "Lainnya", mohon isi keterangannya.' class FinalResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): compute_round4_outcomes(group) compute_rank_guess_payments(group) compute_random_main_payment(group) compute_final_total_payoff(group) class ThankYou(Page): @staticmethod def vars_for_template(player: Player): round_name_map = { 1: 'Pertama', 2: 'Kedua', 3: 'Ketiga', 4: 'Keempat', } rank_name_map = { 1: 'Pertama', 2: 'Kedua', 3: 'Ketiga', 4: 'Keempat', } if player.round3_scheme == 'piece_rate': round3_choice_text = 'Bayaran per Jawaban Benar' else: round3_choice_text = 'Kompetisi' return dict( round1_guess_text=rank_name_map.get(player.round1_rank_guess, ''), round2_guess_text=rank_name_map.get(player.round2_rank_guess, ''), round3_choice_text=round3_choice_text, ) page_sequence = [ Welcome, Code, Wait1, Round1, Wait2, Round1Task, Round1Result, Round2, Wait3, Round2Task, Round2ResultsWaitPage, Round2Results, Round3, Round3Choice, Wait4, Round3Task, Round3Results, Round4, Round4Choice, RankGuess, LotteryTask, LotteryWaitPage, LotteryOutcome, FinalQuestionnaire, FinalResultsWaitPage, ThankYou ]