# lottery_app/pages.py from otree.api import * from .models import Constants, Player import json # 小工具:從 participant.vars 取出 9 回合玩法,並切出對應 block 的 3 個 def get_block_play_types(participant, block_index): """ block_index = 0,1,2 對應: 0 -> 回合 1-3 1 -> 回合 4-6 2 -> 回合 7-9 回傳:list 長度 3,例如 ['拉霸', '數字玩法', '轉盤'] """ play_types = participant.vars.get('play_types_per_round', []) # 防呆:長度不夠就補 '?' play_types = (play_types + ['?'] * 9)[:9] start = block_index * 3 end = start + 3 return play_types[start:end] def ai_is_yes(player: Player) -> bool: """ 從 participant.vars 讀取 AI 條件。 你只需要把 'ai_condition' 改成你實際存的 key。 可接受:'yes'/'no'、True/False、1/0、'是'/'否' """ v = player.participant.vars.get('ai_condition', None) if v is None: return False if isinstance(v, bool): return v if isinstance(v, (int, float)): return v == 1 if isinstance(v, str): return v.strip().lower() in ['yes', 'y', 'true', '1', '是', 'ai=是', 'ai'] return False class PostTask_AI_Check(Page): form_model = 'player' form_fields = ['ai_used'] class PostTask_AI_Detail(Page): form_model = 'player' form_fields = ['ai_assist_methods'] def is_displayed(self): return self.player.field_maybe_none('ai_used') == 'yes' class PostTask_Strategy(Page): form_model = 'player' form_fields = ['inst_clarity', 'inst_clarity_why','study_purpose_guess', 'strategy_used','stats_edu',] class PostTask_Ratings(Page): form_model = 'player' # 這裡必須包含所有在 HTML 裡出現的 input name form_fields = [ # 1-7 分題目的評分與原因 'diff_overall', 'diff_overall_why', 'diff_wheel', 'diff_wheel_why', 'diff_slot', 'diff_slot_why', 'diff_number', 'diff_number_why', # 1-5 分題目的評分與原因 (注意名稱必須與 models.py 一致) 'option_increase_diff', 'option_increase_diff_why', 'change_playtype_easier', 'change_playtype_easier_why', 'page_switch_easier', 'page_switch_easier_why', # 其他題目 'willing_to_pay', 'other_comments' ] def vars_for_template(self): return dict( rating_items=[ dict(field='diff_overall', label='Please evaluate the difficulty of the entire Lottery Choice task', scale=7), dict(field='diff_wheel', label='Please evaluate the difficulty of choosing lotteries of Spinner', scale=7), dict(field='diff_slot', label='Please evaluate the difficulty of choosing lotteries of Slot Machine', scale=7), dict(field='diff_number', label='Please evaluate the difficulty of choosing lotteries of Drawing a Number', scale=7), dict(field='option_increase_diff', label='Did increasing the number of options make the choice more difficult?', scale=5), dict(field='change_playtype_easier', label='Would switching the lottery format make the choices easier?', scale=5), dict(field='page_switch_easier', label='Would displaying each choice separately make the choices easier? ', scale=5), ] ) # Actual Pages # # class consent(Page): form_model = 'player' form_fields = ['consent_electronic'] class instructions(Page): pass class PVT_Experiment_Instruction(Page): pass class PVTPostTestInstructions(Page): pass class RiskPreferenceInstructions(Page): form_model = 'player' form_fields = [] # 說明頁,不蒐集答案 class Intro(Page): form_model = 'player' form_fields = ['quiz_num_prob', 'quiz_wheel_prob', 'quiz_slot_prob', 'payrule_q1', 'payrule_q2', 'payrule_q3', 'payrule_q4'] def error_message(self, values): solutions = { 'quiz_num_prob': 50, 'quiz_wheel_prob': 60, 'quiz_slot_prob': 40, 'payrule_q1': False, 'payrule_q2': True, 'payrule_q3': True, 'payrule_q4': False, } error_fields_map = { 'quiz_num_prob': 'error_count_num', 'quiz_wheel_prob': 'error_count_wheel', 'quiz_slot_prob': 'error_count_slot', 'payrule_q1': 'error_count_payrule1', 'payrule_q2': 'error_count_payrule2', 'payrule_q3': 'error_count_payrule3', 'payrule_q4': 'error_count_payrule4', } prob_fields = ['quiz_num_prob', 'quiz_wheel_prob', 'quiz_slot_prob'] payrule_fields = ['payrule_q1', 'payrule_q2', 'payrule_q3', 'payrule_q4'] errors = {} any_error = False payrule_has_error = False for field, solution in solutions.items(): if values[field] != solution: any_error = True counter_name = error_fields_map[field] setattr(self.player, counter_name, getattr(self.player, counter_name) + 1) if field in prob_fields: errors[field] = 'Your answer is incorrect.' elif field in payrule_fields: payrule_has_error = True if payrule_has_error: errors['payrule_q1'] = 'One or more of your answers in Question 4 are incorrect. Please review and try again.' if any_error: self.player.quiz_error_count += 1 return errors # ---- Single UI:3 頁,每頁 3 個 round ---- class SingleBlock1(Page): form_model = 'player' form_fields = ['s1', 's2', 's3', 'click_timestamps1_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'single' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=0) return dict( block_index=1, # 第一個 block(回合 1-3) block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) class SingleBlock2(Page): form_model = 'player' form_fields = ['s4', 's5', 's6', 'click_timestamps2_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'single' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=1) return dict( block_index=2, # 第二個 block(回合 4-6) block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) class SingleBlock3(Page): form_model = 'player' form_fields = ['s7', 's8', 's9', 'click_timestamps3_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'single' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=2) return dict( block_index=3, # 第三個 block(回合 7-9) block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) # ---- Grouped UI:維持原本三頁,但每頁都顯示對應 block 的玩法 ---- class G1(Page): form_model = 'player' form_fields = ['g1a', 'g1b', 'g1c', 'click_timestamps1_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'grouped' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=0) return dict( block_index=1, # 第一頁 = 回合 1-3 block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) class G2(Page): form_model = 'player' form_fields = ['g2a', 'g2b', 'g2c', 'click_timestamps2_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'grouped' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=1) return dict( block_index=2, # 第二頁 = 回合 4-6 block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) class G3(Page): form_model = 'player' form_fields = ['g3a', 'g3b', 'g3c', 'click_timestamps3_json'] def is_displayed(self): return self.player.participant.vars.get('ui_mode') == 'grouped' def vars_for_template(self): block_types = get_block_play_types(self.player.participant, block_index=2) return dict( block_index=3, # 第三頁 = 回合 7-9 block_types=block_types, block_types_str="、".join(block_types), treatment=self.player.participant.vars.get('treatment', 'unknown'), ) def play_type_to_mode(play_type: str) -> str: pt = (play_type or "").strip() if "轉盤" in pt: return "wheel" if "拉霸" in pt: return "slot" return "number" class RiskElicitation(Page): form_model = 'player' form_fields = [f'hl_{i}' for i in range(1, 11)] def before_next_page(self, timeout_happened=True): # 原本彩券部分的獎金已經在 ensure_payoff_draw 算過了 # 現在加上風險測驗的 10% 隨機獎勵 self.player.calc_hl_payoff() # 解決 KeyError:在此處指定隱藏欄位 def get_form_widgets(self): return {f'hl_{i}': HiddenInput() for i in range(1, 11)} # 解決 UndefinedVariable:把清單傳給 HTML def vars_for_template(self): hl_data = [ (1, "1/10 獲得 2.00元, 9/10 獲得 1.60元", "1/10 獲得 3.85元, 9/10 獲得 0.10元"), (2, "2/10 獲得 2.00元, 8/10 獲得 1.60元", "2/10 獲得 3.85元, 8/10 獲得 0.10元"), (3, "3/10 獲得 2.00元, 7/10 獲得 1.60元", "3/10 獲得 3.85元, 7/10 獲得 0.10元"), (4, "4/10 獲得 2.00元, 6/10 獲得 1.60元", "4/10 獲得 3.85元, 6/10 獲得 0.10元"), (5, "5/10 獲得 2.00元, 5/10 獲得 1.60元", "5/10 獲得 3.85元, 5/10 獲得 0.10元"), (6, "6/10 獲得 2.00元, 4/10 獲得 1.60元", "6/10 獲得 3.85元, 4/10 獲得 0.10元"), (7, "7/10 獲得 2.00元, 3/10 獲得 1.60元", "7/10 獲得 3.85元, 3/10 獲得 0.10元"), (8, "8/10 獲得 2.00元, 2/10 獲得 1.60元", "8/10 獲得 3.85元, 2/10 獲得 0.10元"), (9, "9/10 獲得 2.00元, 1/10 獲得 1.60元", "9/10 獲得 3.85元, 1/10 獲得 0.10元"), (10, "10/10 獲得 2.00元, 0/10 獲得 1.60元", "10/10 獲得 3.85元, 0/10 獲得 0.10元"), ] return dict(hary_laury_list=hl_data) class PayoffDraw(Page): form_model = 'player' form_fields = ['payoff_clicked'] def is_displayed(self): return True def vars_for_template(self): p = self.player # 1. 保存選擇並執行抽獎邏輯 p.save_choices_json() p.ensure_payoff_draw() # 2. 取得抽中那一回合的玩法資訊 idx0 = p.payoff_round - 1 play_types = (p.participant.vars.get('play_types_per_round', []) + ['?'] * 9)[:9] play_type = play_types[idx0] mode = play_type_to_mode(play_type) # 3. 取得該回合選中彩券的資料 t_final = p._get_ticket_dict(idx0, p.payoff_ticket_id) or {} def f(x): return Player._to_float(x) # 4. 根據 models.py 標準化的 P0-P4 欄位抓取資料 # 注意:Key 必須與 game_vis.js 的 ORDER [0, 1.5, 3, 9, 15] 一致 entries = [ {'key': 0, 'prob': f(t_final.get('P0', 0))}, {'key': 1.5, 'prob': f(t_final.get('P1', 0))}, {'key': 3, 'prob': f(t_final.get('P2', 0))}, {'key': 9, 'prob': f(t_final.get('P3', 0))}, {'key': 15, 'prob': f(t_final.get('P4', 0))}, ] cfg = { 'rootId': 'payoff-draw-root', 'mode': mode, 'entries': entries, 'finalNumber': p.payoff_number, 'finalPrize': p.payoff_prize, 'storageKey': f'lottery_payoff_played_{p.participant.code}', 'unlockNext': True, } return dict( drawn_round=p.payoff_round, play_type=play_type, payoff_draw_config_json=json.dumps(cfg, ensure_ascii=False), ) class FinalResults(Page): def vars_for_template(self): showup = float(Constants.showup_fee) p2_bonus = float(self.player.hl_won_prize) p3_bonus = float(self.player.payoff_prize) # ← 改這行,不用 self.player.payoff total = showup + p2_bonus + p3_bonus return { 'p3_bonus_clean': "{:.2f}".format(p3_bonus), 'total_amount_all': "{:.2f}".format(total), } class PVT_Pre(Page): template_name = 'lottery_app/PVT_Task.html' form_model = 'player' form_fields = ['pvt_pre_results'] def vars_for_template(self): # 使用 self.player 可以確保一定拿得到目前的受試者資料 return { 'is_post_test': False } class PVT_Post(Page): template_name = 'lottery_app/PVT_Task.html' form_model = 'player' form_fields = ['pvt_post_results'] def vars_for_template(self): return { 'is_post_test': True } # ---- VAS-F 18 題資料(題號、欄位名、左錨、右錨)---- VASF_ITEMS = [ dict(num=1, field_name='vasf_{phase}_1', keyword='Tired', anchor_left='not at all tired', anchor_right='extremely tired'), dict(num=2, field_name='vasf_{phase}_2', keyword='Sleepy', anchor_left='not at all sleepy', anchor_right='extremely sleepy'), dict(num=3, field_name='vasf_{phase}_3', keyword='Drowsy', anchor_left='not at all drowsy', anchor_right='extremely drowsy'), dict(num=4, field_name='vasf_{phase}_4', keyword='Fatigued', anchor_left='not at all fatigued', anchor_right='extremely fatigued'), dict(num=5, field_name='vasf_{phase}_5', keyword='Worn out', anchor_left='not at all worn out', anchor_right='extremely worn out'), dict(num=6, field_name='vasf_{phase}_6', keyword='Energetic', anchor_left='not at all energetic', anchor_right='extremely energetic'), dict(num=7, field_name='vasf_{phase}_7', keyword='Active', anchor_left='not at all active', anchor_right='extremely active'), dict(num=8, field_name='vasf_{phase}_8', keyword='Vigorous', anchor_left='not at all vigorous', anchor_right='extremely vigorous'), dict(num=9, field_name='vasf_{phase}_9', keyword='Efficient', anchor_left='not at all efficient', anchor_right='extremely efficient'), dict(num=10, field_name='vasf_{phase}_10', keyword='Lively', anchor_left='not at all lively', anchor_right='extremely lively'), dict(num=11, field_name='vasf_{phase}_11', keyword='Bushed', anchor_left='not at all bushed', anchor_right='totally bushed'), dict(num=12, field_name='vasf_{phase}_12', keyword='Exhausted', anchor_left='not at all exhausted', anchor_right='totally exhausted'), dict(num=13, field_name='vasf_{phase}_13', keyword='Keeping eyes open', anchor_left='keeping my eyes open is no effort at all', anchor_right='keeping my eyes open is a tremendous chore'), dict(num=14, field_name='vasf_{phase}_14', keyword='Moving body', anchor_left='moving my body is no effort at all', anchor_right='moving my body is a tremendous chore'), dict(num=15, field_name='vasf_{phase}_15', keyword='Concentrating', anchor_left='concentrating is no effort at all', anchor_right='concentrating is a tremendous chore'), dict(num=16, field_name='vasf_{phase}_16', keyword='Carrying on a conversation', anchor_left='carrying on a conversation is no effort at all', anchor_right='carrying on a conversation is a tremendous chore'), dict(num=17, field_name='vasf_{phase}_17', keyword='Desire to close eyes', anchor_left='I have absolutely no desire to close my eyes', anchor_right='I have a tremendous desire to close my eyes'), dict(num=18, field_name='vasf_{phase}_18', keyword='Desire to lie down', anchor_left='I have absolutely no desire to lie down', anchor_right='I have a tremendous desire to lie down'), ] def get_vasf_items(phase): """phase = 'pre' 或 'post',回傳帶有正確 field_name 的 item list""" result = [] for item in VASF_ITEMS: d = dict(item) d['field_name'] = item['field_name'].format(phase=phase) result.append(d) return result class PostTask_Uncertainty(Page): form_model = 'player' form_fields = ['choice_uncertainty'] class VASF_Pre(Page): template_name = 'lottery_app/VASF_Task.html' form_model = 'player' form_fields = [f'vasf_pre_{i}' for i in range(1, 19)] def vars_for_template(self): return dict( items=get_vasf_items('pre'), is_post_test=False, ) def error_message(self, values): # 確保每一題都有作答(不等於初始值 -1) #missing = [i for i in range(1, 19) if values.get(f'vasf_pre_{i}', -1) == -1] #if missing: #return 'Please respond to all items before continuing.' return None class VASF_Post(Page): template_name = 'lottery_app/VASF_Task.html' form_model = 'player' form_fields = [f'vasf_post_{i}' for i in range(1, 19)] def vars_for_template(self): return dict( items=get_vasf_items('post'), is_post_test=True, ) def error_message(self, values): missing = [i for i in range(1, 19) if values.get(f'vasf_post_{i}', -1) == -1] if missing: return 'Please respond to all items before continuing.' # ---- Results ---- class Redirect(Page): def is_displayed(self): return True def vars_for_template(self): completionlink = self.player.participant.vars.get('prolific_completion_url', None) return dict(completionlink=completionlink) def js_vars(self): return dict(completionlink=self.player.subsession.session.config['completionlink']) page_sequence = [ consent, instructions, RiskPreferenceInstructions, RiskElicitation, VASF_Pre,# ← 前測疲勞(取代 PVT_Pre) Intro, SingleBlock1, SingleBlock2, SingleBlock3, G1, G2, G3, VASF_Post, # ← 後測疲勞(取代 PVT_Post) PostTask_Uncertainty, PostTask_AI_Check, PostTask_AI_Detail, PostTask_Strategy, PostTask_Ratings, PayoffDraw, FinalResults, Redirect, ]