import csv import math import random import time from datetime import datetime from itertools import chain from numpy import linspace from otree.api import * import json doc = """ Your app description """ def get_table_list(file_name: str): """ 从文件中读取表格的列表 """ with open('tradeoff/table_list/' + file_name, 'r',encoding='utf-8') as file: countriesStr = file.read() table_list = eval(countriesStr) return table_list.copy() def prob_str_to_num(prob_str): """ 把字符形式的概率转为数字 """ return [eval(i) for i in json.loads(prob_str)] class Constants(BaseConstants): name_in_url = 'tradeoff' players_per_group = None num_rounds = 36 # 见设计excel,prob_1_3即以1/3开头的那一行概率 prob_str_1_6 = ['1/6', '5/6', '1/6', '5/6'] prob_str_1_3 = ['1/3', '2/3', '1/3', '2/3'] prob_str_1_2 = ['1/2', '1/2', '1/2', '1/2'] prob_str_2_3 = ['2/3', '1/3', '2/3', '1/3'] prob_str_5_6 = ['5/6', '1/6', '5/6', '1/6'] prob_str_list = [ prob_str_1_6, prob_str_1_3, prob_str_1_2, prob_str_2_3, prob_str_5_6 ] class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): if subsession.round_number == 1: # debug状态,不设置则为False subsession.session.vars['debug'] = subsession.session.config.get('debug', False) # 读取顺序表 table_list_file = subsession.session.config['table_list'] table_list_raw: list = get_table_list(table_list_file) # print(f'table_list_raw: {table_list_raw}') do_shuffle = subsession.session.config['random'] # if subsession.session.config['table_list'] =='table_list_mix.py' or 'table_list_test.py': # is_mix = True # else: # is_mix = False # print(f"n player: {len(subsession.get_players())}") p: Player for p in subsession.get_players(): table_list_raw_p = table_list_raw.copy() # 21.11.9 新增混合版本随机需求: if do_shuffle: for x in table_list_raw_p: random.shuffle(x) for i in x: random.shuffle(i) # else: # for x in table_list_raw_p: # random.shuffle(x) # print(f'{p.id_in_subsession}: {table_list_raw_p}') p.participant.vars['table_list_raw'] = table_list_raw_p.copy() # print(f"{p.id_in_subsession}: {p.participant.vars['table_list_raw']}") #21.11.9 新增混合版本随机需求: # if is_mix: table_list = [c for a in table_list_raw_p for b in a for c in b] # else: # table_list = list(chain.from_iterable(table_list_raw_p)) p.participant.vars['table_list'] = table_list.copy() # print(f'{p.id_in_subsession}: {table_list}') p.participant.vars['table_list_show'] = list(enumerate(table_list)) # 表格的数量,就是round的数量 p.participant.vars['num_rounds'] = len(table_list) assert len(table_list) <= Constants.num_rounds # 读取等待时间 subsession.session.vars['intro_wait_sec'] = subsession.session.config.get('intro_wait_sec', 0) subsession.session.vars['table_wait_sec'] = subsession.session.config.get('table_wait_sec', 0) class Group(BaseGroup): pass def make_data_field(): return models.FloatField(default=10086, max=10000) def get_value_list(lower, upper): value_list = [round(i, 2) for i in list(linspace(lower, upper, 21))] return value_list class Player(BasePlayer): table_id = models.IntegerField() sp = make_data_field() lower = make_data_field() upper = make_data_field() prob_str = models.StringField() col_1 = make_data_field() col_2 = make_data_field() col_3 = make_data_field() col_4 = make_data_field() num_rounds = models.IntegerField() # 问卷变量 phonenumber = models.IntegerField() name = models.StringField() gender = models.StringField() major = models.StringField() grade = models.StringField( choices=['大一', '大二', '大三', '大四', '研一', '研二', '研三', '其它'], widget=widgets.RadioSelectHorizontal ) GPA = models.FloatField() E1 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E2 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E3 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E4 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E5 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E6 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E7 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E8 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E9 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E10 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E11 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) E12 = models.IntegerField( label='', choices=['1', '2', '3', '4', '5', '6', '7', '8'], widget=widgets.RadioSelectHorizontal ) def format_number(x, digits=0): """ 有效数字 ,整数去掉".0" 1.0 -> "1" [8.0, 1.51] -> ["8", "1.5"] """ if type(x) == int: return x if type(x) == float: return '{:g}'.format(round(x, digits)) return [None if i is None else '{:g}'.format(round(i, digits)) for i in x] # def cls_to_id(cls): # """ # 从类名中获得id # """ # name = cls.__name__ # table_id = int(name.replace("Table", "")) # return table_id # PAGES def get_table_id(player): # print(f"table_list: {player.participant.vars['table_list']}") # print(f"player.round_number: {player.round_number}") if player.subsession.session.config['table_list'] =='table_list_mix.py': return player.participant.vars['table_list'][player.round_number - 1] return player.participant.vars['table_list'][player.round_number - 1] class TablePage(Page): form_model = 'player' form_fields = ['sp'] # prob_str = [] # table_id = 0 # lower = 0 # upper = 0 # col_1 = None # col_2 = None # col_3 = None # col_4 = None # @classmethod # def save_page_var(cls, player, var_name, value): # player.participant.vars[var_name + '_' + str(cls.table_id)] = value # @classmethod # def before_next_page(cls, player: Player, timeout_happened): # # 后台数据取整,保持界面和数据一致 # # player.participant.vars['sp_' + str(cls.table_id)] = getattr(player, 'sp') # # print('sp_' + str(cls.table_id)) # # time.sleep(0.2) # # if not ('sp_' + str(cls.table_id)) in player.participant.vars: # # print("error") # # player.participant.vars['sp_' + str(cls.table_id)] = getattr(player, 'sp') # # cls.save_page_var(player, "sp", getattr(player, 'sp')) # cls.save_page_var(player, "col_1", cls.col_1) # cls.save_page_var(player, "col_2", cls.col_2) # cls.save_page_var(player, "col_3", cls.col_3) # cls.save_page_var(player, "col_4", cls.col_4) # cls.save_page_var(player, "prob", cls.prob_str.copy()) @staticmethod def is_displayed(player: Player): # print(cls.table_id) # return True is_correct_round = player.round_number <= player.participant.vars['num_rounds'] if not is_correct_round: return False is_correct_order = player.table_id == get_table_id(player) return is_correct_order def table_id_to_row(table_id): return table_id % 100 # ===================== # Part 1 左侧3表 # ===================== def table_is_left(table_id): return str(table_id)[1] == "1" def table_id_to_part(table_id): return math.floor(table_id / 1000) def is_show(player: Player, part: int, is_left: bool): """ 判断是否应该显示本表 """ # 条件3:如果超了轮次,直接返回 is_correct_round = player.round_number <= player.participant.vars['num_rounds'] if not is_correct_round: return False table_id = get_table_id(player) # print(f"check {table_id}") # 条件1:第几个Part? if table_id_to_part(table_id) != part: return False # 条件2:左表还是右表? if not table_is_left(table_id) == is_left: return False return True class P1Left(TablePage): """ Part I 左表 """ @staticmethod def vars_for_template(player: Player): # cls.table_id = cls_to_id(cls) player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) prob_srt_list = [Constants.prob_str_1_3, Constants.prob_str_1_2, Constants.prob_str_2_3] # 左右两个表的prob相同,因此用本表的prob即可 player.prob_str = json.dumps(prob_srt_list[row - 1].copy()) upper_list = [90, 100, 120] player.upper = upper_list[row - 1] player.lower = 80 value_list = get_value_list(player.lower, player.upper) player.col_1 = 60 player.col_2 = 80 player.col_3 = 50 return dict( table_id=player.table_id, value_list=value_list, value_str_list=format_number(value_list, digits=1), prob_str=json.loads(player.prob_str), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_3=format_number(player.col_3) ) @staticmethod def is_displayed(player): # print(cls.table_id) # return True return is_show(player, part=1, is_left=True) # ===================== # Part 1 右侧3表 # ===================== def load_sp(player: Player, table_id): """ 根据table_id读取sp 如果出现KeyError,打印参与人信息,并再试一次 """ player_list = player.in_previous_rounds() target_list = [p for p in player_list if p.table_id == table_id] target: Player = target_list[0] return target.sp class P1Right(TablePage): """ Part II 右表 """ @staticmethod def vars_for_template(player: Player): player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) # 减去100得左侧id # 1201 - 100 = 1101 ref_id_left = player.table_id - 100 pre_sp = load_sp(player, ref_id_left) # 左侧表的sp prob_srt_list = [Constants.prob_str_1_3, Constants.prob_str_1_2, Constants.prob_str_2_3] # 左右两个表的prob相同,因此用本表的prob即可 player.prob_str = json.dumps(prob_srt_list[row - 1]) prob = prob_str_to_num(player.prob_str) # 区间值改为取整 player.lower = format_number(pre_sp) player.upper = format_number(pre_sp) + (60 - 50) / (prob[3] / prob[2]) * 2 # setattr(player, "upper", cls.upper) # player.participant.vars["upper_" + str(cls.table_id)] = cls.upper # save_lower_upper(player, cls) value_list = get_value_list(player.lower, player.upper) player.col_1 = 60 player.col_2 = pre_sp player.col_3 = 50 return dict( table_id=player.table_id, prob_str=json.loads(player.prob_str), # pre_sp=format_number(pre_sp), value_list=value_list, value_str_list=format_number(value_list, digits=1), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_3=format_number(player.col_3) ) @staticmethod def is_displayed(player): # print(cls.table_id) # return True return is_show(player, part=1, is_left=False) class P2Left(TablePage): """ Part II 左表 """ @staticmethod def vars_for_template(player: Player): # cls.table_id = cls_to_id(cls) player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) # col_4 计算 =================== # 2011 ~ 2051,即row >= 1 & row <= 5,对应1201的col_2,即1101的sp(sp_1101) # 2061 ~ 2101,即row >= 6 & row <= 10,对应1202的col_2,即1102的sp(sp_1102) # 2111 ~ 2151,即row >= 11 & row <= 15,对应1203的col_2,即1102的sp(sp_1103) # def p2row_to_ref_id(the_row): # i = math.ceil(the_row / 5) # return 1100 + i # # assert p2row_to_ref_id(1) == 1101 # assert p2row_to_ref_id(5) == 1101 # assert p2row_to_ref_id(15) == 1103 # # ref_id = p2row_to_ref_id(row) col_4_list = [85, 90, 100] player.col_4 = col_4_list[math.ceil(row / 5) - 1] # 获取col_4:即黄绿红格子 # player.col_4 = load_sp(player, ref_id) player.prob_str = json.dumps(Constants.prob_str_list[row % 5 - 1]) prob = prob_str_to_num(player.prob_str) player.upper = 60 player.lower = round(60 - (player.col_4 - 80) / (prob[2] / prob[3]) * 2, 1) ## save_lower_upper(player, cls) value_list = get_value_list(player.lower, player.upper) player.col_1 = 60 player.col_2 = 80 return dict( table_id=player.table_id, value_list=value_list, value_str_list=format_number(value_list, digits=1), prob_str=json.loads(player.prob_str), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_4=format_number(player.col_4), ) @staticmethod def is_displayed(player): return is_show(player, part=2, is_left=True) class P2Right(TablePage): """ Part II 右表 """ @staticmethod def vars_for_template(player): # player.table_id = cls_to_id(cls) player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) # col_4 计算 =================== # 2011 ~ 2051,即row >= 1 & row <= 5,对应1201的col_2,即1101的sp(sp_1101) # 2061 ~ 2101,即row >= 6 & row <= 10,对应1202的col_2,即1102的sp(sp_1102) # 2111 ~ 2151,即row >= 11 & row <= 15,对应1203的col_2,即1102的sp(sp_1103) # def p2row_to_ref_id(row): # i = math.ceil(row / 5) # return 1100 + i # # assert p2row_to_ref_id(1) == 1101 # assert p2row_to_ref_id(5) == 1101 # assert p2row_to_ref_id(15) == 1103 player.col_1 = 60 # # 获取col_2:Part 1 对应的sp # ref_id = p2row_to_ref_id(row) # player.col_2 = load_sp(player, ref_id) col_2_list = [85, 90, 100] player.col_2 = col_2_list[math.ceil(row / 5) - 1] # 获取col_3:左表的sp pre_table_id = player.table_id - 100 player.col_3 = load_sp(player, pre_table_id) player.prob_str = json.dumps(Constants.prob_str_list[row % 5 - 1]) prob = prob_str_to_num(player.prob_str) player.lower = round(player.col_2) # 新增取整 player.upper = round(player.col_2) + (60 - round(player.col_3))/ (prob[3] / prob[2]) * 2 # 新增取整 # save_lower_upper(player, cls) value_list = get_value_list(player.lower, player.upper) return dict( table_id=player.table_id, value_list=value_list, value_str_list=format_number(value_list, digits=1), prob_str=json.loads(player.prob_str), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_3=format_number(player.col_3) ) @staticmethod def is_displayed(player): return is_show(player, part=2, is_left=False) class P3Left(TablePage): """ Part III 左表 """ @staticmethod def vars_for_template(player: Player): # cls.table_id = cls_to_id(cls) player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) # col_4 计算 =================== # 2011 ~ 2051,即row >= 1 & row <= 5,对应1201的col_2,即1101的sp(sp_1101) # 2061 ~ 2101,即row >= 6 & row <= 10,对应1202的col_2,即1102的sp(sp_1102) # 2111 ~ 2151,即row >= 11 & row <= 15,对应1203的col_2,即1102的sp(sp_1103) # def p2row_to_ref_id(row): # i = math.ceil(row / 5) # return 1100 + i # # assert p2row_to_ref_id(1) == 1101 # assert p2row_to_ref_id(5) == 1101 # assert p2row_to_ref_id(15) == 1103 # # ref_id = p2row_to_ref_id(row) # # # 获取col_4:即黄绿红格子 # player.col_4 = load_sp(player, ref_id) player.prob_str = json.dumps(Constants.prob_str_list[row % 5 - 1]) prob = prob_str_to_num(player.prob_str) # 第一列可变 col_1_list = [105, 110, 120] player.col_1 = col_1_list[math.ceil(row / 5) - 1] # 新增第四列可变 col_4_list = [85, 90, 100] player.col_4 = col_4_list[math.ceil(row / 5) - 1] player.upper = player.col_1 player.lower = round(player.col_1 - (player.col_4 - 80) / (prob[2] / prob[3]) * 2, 1) # save_lower_upper(player, cls) value_list = get_value_list(player.lower, player.upper) # print(f"row: {row}") # print(f"col_1: {col_1}") # print(f"col_4: {col_4}") player.col_2 = 80 return dict( table_id=player.table_id, value_list=value_list, value_str_list=format_number(value_list, digits=1), prob_str=json.loads(player.prob_str), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_4=format_number(player.col_4), ) @staticmethod def is_displayed(player): return is_show(player, part=3, is_left=True) class P3Right(TablePage): """ Part III 右表 """ @staticmethod def vars_for_template(player: Player): # player.table_id = cls_to_id(cls) player.table_id = get_table_id(player) row = table_id_to_row(player.table_id) # col_4 计算 =================== # 2011 ~ 2051,即row >= 1 & row <= 5,对应1201的col_2,即1101的sp(sp_1101) # 2061 ~ 2101,即row >= 6 & row <= 10,对应1202的col_2,即1102的sp(sp_1102) # 2111 ~ 2151,即row >= 11 & row <= 15,对应1203的col_2,即1102的sp(sp_1103) # def p2row_to_ref_id(row): # i = math.ceil(row / 5) # return 1100 + i # # assert p2row_to_ref_id(1) == 1101 # assert p2row_to_ref_id(5) == 1101 # assert p2row_to_ref_id(15) == 1103 # col_1 col_1_list = [105, 110, 120] player.col_1 = col_1_list[math.ceil(row / 5) - 1] # 新增第2列可变 col_2_list = [85, 90, 100] player.col_2 = col_2_list[math.ceil(row / 5) - 1] # 获取col_2:Part 1 对应的sp # ref_id = p2row_to_ref_id(row) # player.col_2 = load_sp(player, ref_id) # 获取col_3:左表的sp pre_table_id = player.table_id - 100 player.col_3 = load_sp(player, pre_table_id) # col_4 player.prob_str = json.dumps(Constants.prob_str_list[row % 5 - 1]) prob = prob_str_to_num(player.prob_str) player.lower = round(player.col_2) # 新增取整 player.upper = round(player.col_2) + (round(player.col_1) - round(player.col_3)) * prob[0] / prob[1] * 2 # 新增取整 # print(f"row: {row}") # print(f"col_1: {player.col_1}") # print(f"col_2: {player.col_2}") # print(f"col_3: {player.col_3}") # print(f"lower: {player.lower}") # print(f"upper: {player.upper}") # save_lower_upper(player, cls) value_list = get_value_list(player.lower, player.upper) return dict( table_id=player.table_id, value_list=value_list, value_str_list=format_number(value_list, digits=1), prob_str=json.loads(player.prob_str), col_1=format_number(player.col_1), col_2=format_number(player.col_2), col_3=format_number(player.col_3) ) @staticmethod def is_displayed(player): return is_show(player, part=3, is_left=False) class Intro0(Page): @staticmethod def vars_for_template(player: Player): pass @staticmethod def is_displayed(player): return player.round_number == 1 class Intro(Page): @staticmethod def vars_for_template(player: Player): pass @staticmethod def is_displayed(player): return player.round_number == 1 class Intro2(Page): @staticmethod def is_displayed(player): return player.round_number == 1 class Question(Page): @staticmethod def is_displayed(player): return player.round_number == 1 class Lottery(Page): i = 0 table_id = 0 row = 0 col_1 = 0 col_2 = 0 col_3 = 0 col_4 = 0 upper = 0 lower = 0 sp = 0 @staticmethod def is_displayed(player: Player): cond = player.round_number == Constants.num_rounds return cond @classmethod def vars_for_template(cls, player): # def get_value_by_id(name: str): # return player.participant.vars[name + "_" + str(cls.table_id)] cls.i = random.randint(1, player.participant.vars['num_rounds']) target: Player = player.in_round(cls.i) cls.table_id = target.table_id cls.row = random.randint(1, 21) cls.sp = target.sp cls.col_1 = target.col_1 cls.col_2 = target.col_2 cls.col_3 = target.col_3 cls.col_4 = target.col_4 var_col = [cls.col_1, cls.col_2, cls.col_3, cls.col_4].index(10086) cls.upper = target.upper cls.lower = target.lower value_list = [round(i, 2) for i in list(linspace(cls.lower, cls.upper, 21))] col_var_value = value_list[cls.row - 1] true_row = format_number([cls.col_1, cls.col_2, cls.col_3, cls.col_4].copy(), 0) true_row[var_col] = format_number(float(col_var_value), 1) is_a = cls.sp > col_var_value prob_str = json.loads(target.prob_str) dice = random.randint(1, 6) prob_x6 = [eval(p) * 6 for p in prob_str] if is_a: dice_threshold = prob_x6[0] if dice <= dice_threshold: result = true_row[0] else: result = true_row[1] else: dice_threshold = prob_x6[2] if dice <= dice_threshold: result = true_row[2] else: result = true_row[3] player.participant.vars["lottery_table_id"] = cls.table_id player.participant.vars["lottery_row"] = cls.row player.participant.vars["lottery_dice"] = dice player.participant.vars["lottery_result"] = result return dict( i=cls.i, row=cls.row, sp=cls.sp, upper=cls.upper, lower=cls.lower, cols=[cls.col_1, cls.col_2, cls.col_3, cls.col_4], var_col=var_col, value_list=format_number(value_list, 1), # var_id = var_id, col_var_value=col_var_value, true_row=true_row, is_a=is_a, is_b=not is_a, prob_str=prob_str, dice=dice, prob_x6=prob_x6, result=result ) class Results(Page): """ 本页的主要作用是保存数据到本地csv文件 """ @staticmethod def is_displayed(player): cond = player.round_number == Constants.num_rounds if cond: player.participant.payoff = round(float(player.participant.vars["lottery_result"]) / 3) player.participant.vars["payoff_fee"] = player.participant.payoff + 10 # 按key排序,便于合并 data = dict(sorted(player.participant.vars.items(), key=lambda x: x[0])) field_names = list(data.keys()) # print(field_names) p_id = player.participant.code now = datetime.now() dt_string = now.strftime("%Y%m%d_%H%M%S") with open(f'output/data_{dt_string}_{p_id}.csv', 'w', newline='') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=field_names) writer.writeheader() # writer.writerows(player.participant.vars) writer.writerow(data) return cond class Survey(Page): form_model = 'player' form_fields = ['phonenumber', 'name', 'gender', 'major', 'grade', 'GPA', 'E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'E9', 'E10', 'E11', 'E12'] @classmethod def is_displayed(cls, player: Player): cond = player.round_number == Constants.num_rounds return cond page_sequence = [ Intro0, Intro, Intro2, Question, # P1Left, # P1Right, P2Left, P2Right, P3Left, P3Right, Survey, Lottery, Results ]