from otree.api import * import settings import numpy as np import json import random import ast import time import string doc = """ This is the app for "Consensus with Chat" experiment. It can be before or after another treatment. """ author = "Mouli Modak" class C(BaseConstants): NAME_IN_URL = 'consensus_chat' PLAYERS_PER_GROUP = None NUM_PARAMS = settings.SESSION_CONFIG_DEFAULTS['num_parameters'] NUM_TYPES = settings.SESSION_CONFIG_DEFAULTS['num_types'] # NUM_ROUNDS = 1 PAIR_COMBOS = settings.SESSION_CONFIG_DEFAULTS['pairs_all_combinations'] NUM_ROUNDS = NUM_PARAMS*(len(PAIR_COMBOS)) TIME_TASK = settings.SESSION_CONFIG_DEFAULTS['group_round_length'] GROUPING_DICT = settings.SESSION_CONFIG_DEFAULTS['groupings_dict'] class Subsession(BaseSubsession): disagreement_q = models.IntegerField() class Group(BaseGroup): exp_round = models.IntegerField() task_number = models.IntegerField() param_number = models.IntegerField() dis_typeAPayoff = models.FloatField() dis_typeBPayoff = models.FloatField() dis_typeCPayoff = models.FloatField() param_order = models.StringField() confirmed = models.IntegerField(initial=0) pair_order = models.StringField(initial="") # Round wise pairing sequence class Player(BasePlayer): mySessionID = models.IntegerField() mytype = models.StringField(initial="") mynumber = models.IntegerField() group_pid = models.IntegerField() starttime = models.FloatField() otherid = models.IntegerField() othertype = models.StringField(initial="") othernumber = models.IntegerField() quantityTime = models.FloatField(initial=-1) quantity = models.FloatField(initial=np.nan) typeAPayoff = models.FloatField(initial=np.nan) typeBPayoff = models.FloatField(initial=np.nan) typeCPayoff = models.FloatField(initial=np.nan, min=-10000) myProposalTimeTaskHistory = models.StringField(initial="") myCancelTimeTaskHistory = models.StringField(initial="") myQuantityTaskHistory = models.StringField(initial="") myTypeAPayoffTaskHistory = models.StringField(initial="") myTypeBPayoffTaskHistory = models.StringField(initial="") myTypeCPayoffTaskHistory = models.StringField(initial="") otherProposalTimeTaskHistory = models.StringField(initial="") otherCancelTimeTaskHistory = models.StringField(initial="") otherQuantityTaskHistory = models.StringField(initial="") otherTypeAPayoffTaskHistory = models.StringField(initial="") otherTypeBPayoffTaskHistory = models.StringField(initial="") otherTypeCPayoffTaskHistory = models.StringField(initial="") timeout_task = models.IntegerField(initial=0) ## Subsession method def setting_group(players_list): players_num = len(players_list) # Total number of players treatmen_num = len(players_list[0].participant.vars["treatmentHistory"]) # Treatment Number reference_matrix = C.GROUPING_DICT[players_num][treatmen_num] # Current matrix of grouping # Create a lookup from label to object label_to_object = {p.participant.vars["mySessionID"]: p for p in players_list} # Build the positioned matrix positioned_matrix = [ [label_to_object.get(label, None) for label in row] for row in reference_matrix ] # Convert to NumPy array positioned_np_matrix = np.array(positioned_matrix) # Get dimensions rows, cols = positioned_np_matrix.shape # print(f"Matrix dimensions: {rows} rows × {cols} columns") # Annotate each player with its group number, type, type number, session ID for i in range(rows): for j in range(cols): pij = positioned_np_matrix[i, j] pij.group_pid = i+1 pij.mynumber = int(j/C.NUM_TYPES)+1 pij.mytype = pij.participant.vars["mytype"] pij.mySessionID = pij.participant.vars["mySessionID"] # Display result # print("CwC") # print(positioned_matrix) return(positioned_matrix) def creating_session(subsession: Subsession): subsession.disagreement_q = subsession.session.config['disagreement_q'] if(subsession.session.config['line_input'] != "None"): equations = json.loads(subsession.session.config['line_input']) else: equations = subsession.session.config['line_dict'] print(equations) # Get the list of all players player_list = subsession.get_players() # Generating groups if subsession.round_number == 1: for p in player_list: p.participant.vars["treatmentHistory"].append("CwC") group_matrix = setting_group(player_list) # Shuffling will be done here, 10/02/25) # Convert final matrix to numpy array and assign to the subsession matrix_np = np.array(group_matrix) # print("Period 1, CwC") # print(matrix_np) subsession.set_group_matrix(matrix_np) else: subsession.group_like_round(subsession.round_number-1) # Assigning other players codes for p in player_list: # Assigning players numbers and group ID for each round if subsession.round_number > 1: p.mynumber = p.in_round(1).mynumber p.group_pid = p.in_round(1).group_pid p.mySessionID = p.in_round(1).mySessionID p.mytype = p.in_round(1).mytype # # Saving participant code of others in group # others_participant_code = [po.participant.code for po in p.get_others_in_group()] # p.participant.vars["othersParticipantID"].append(others_participant_code) # Generate order of params params_list = list(range(1, C.NUM_PARAMS + 1)) # for p in player_list: # if p.session.config["position_chat_apps"] == "first": # if "dictator_chat" == p.session.config["app_sequence"][1]: # p.pair_order = str(p.participant.vars["pair_order_1"]) # elif "dictator_chat" == p.session.config["app_sequence"][2]: # p.pair_order = str(p.participant.vars["pair_order_2"]) # else: # if "dictator_chat" == p.session.config["app_sequence"][2]: # p.pair_order = str(p.participant.vars["pair_order_1"]) # elif "dictator_chat" == p.session.config["app_sequence"][3]: # p.pair_order = str(p.participant.vars["pair_order_2"]) treatment_round = int((subsession.round_number-1)/C.NUM_PARAMS) group_list = subsession.get_groups() # Generate random orderings of player pairs for each group pair_order_random = list(range(1, len(C.PAIR_COMBOS) + 1)) # Assigning group level variables for g in group_list: players_in_group = g.get_players() # print("Last App Round Number", players_in_group[0].participant.vars["last_app_round_end"]) # Setting exp_round and task_number if players_in_group[0].participant.vars["last_app_round_end"] != 0: if g.round_number == 1: g.exp_round = players_in_group[0].participant.vars["last_app_round_end"] + 1 else: g.exp_round = players_in_group[0].participant.vars["last_app_round_end"] + 1 + treatment_round else: g.exp_round = 1 + treatment_round g.task_number = int(((g.round_number-1)%C.NUM_PARAMS)) + 1 if g.round_number == 1: # Shuffle one pair order list and assign to each player (first use) pair_12 = pair_order_random[:] random.shuffle(pair_12) g.pair_order = str(pair_12) else: g.pair_order = g.in_round(1).pair_order g_pair_order = ast.literal_eval(g.pair_order) pairs_current_round = C.PAIR_COMBOS[g_pair_order[treatment_round] - 1] for pair in pairs_current_round: x_id = pair[0] y_id = pair[1] px = g.get_player_by_id(x_id) py = g.get_player_by_id(y_id) px.otherid = y_id px.othertype = py.mytype px.othernumber = py.mynumber py.otherid = x_id py.othertype = px.mytype py.othernumber = px.mynumber if g.round_number == C.NUM_ROUNDS: px.participant.vars["last_app_round_end"] = g.exp_round py.participant.vars["last_app_round_end"] = g.exp_round if g.task_number == 1: # Generate random order of params random_params_list = params_list[:] random.shuffle(random_params_list) g.param_order = str(random_params_list) g.param_number = random_params_list[0] else: param_order_text = g.in_round(subsession.round_number-1).param_order g.param_order = param_order_text param_order = ast.literal_eval(param_order_text) g.param_number = param_order[g.task_number - 1] group_equation = equations[g.param_number - 1] print("Group Equation:", group_equation) x = g.subsession.disagreement_q print(x) g.dis_typeAPayoff = eval(group_equation["A"]) g.dis_typeBPayoff = eval(group_equation["B"]) g.dis_typeCPayoff = eval(group_equation["C"]) print(g.dis_typeAPayoff) print(g.dis_typeBPayoff) print(g.dis_typeCPayoff) ## Extra Functions class Message(ExtraModel): session_code = models.StringField() exp_round = models.IntegerField() task_number = models.IntegerField() group_id = models.IntegerField() sender_id_group = models.IntegerField() partner_id_group = models.IntegerField() sender_participant_code = models.StringField() time_remaining = models.IntegerField() sender_nickname = models.StringField() sender_confirmed = models.IntegerField() text = models.StringField() def to_dict(msg: Message, me): ret_dict = dict( action_app = 'Chat_Refresh', nickname = msg.sender_nickname, me = me, text = msg.text, group_id = msg.group_id, sender_id_group = msg.sender_id_group, ) return ret_dict ## PAGES class P1_Intro_CwC(Page): @staticmethod def vars_for_template(player:Player): return dict( mytype = player.mytype, othertype = "B", exp_round = player.group.in_round(1).exp_round + 1, task_number = 4, exp_round_first = player.group.in_round(1).exp_round, exp_round_last = player.group.in_round(C.NUM_ROUNDS).exp_round, time_group_task = C.TIME_TASK, disagreement_quantity = player.subsession.disagreement_q, ) @staticmethod def js_vars(player): if(player.session.config['line_input'] != "None"): equations = json.loads(player.session.config['line_input']) else: equations = player.session.config['line_dict'] return dict( equations=equations, colors=player.session.config['graph_colors'], mytype=player.mytype, othertype="B", otherQuantity = random.randint(0, 100)/10, num_parameter_sets=len(equations), totalTime=player.session.config['individual_round_length']*C.NUM_PARAMS, exp_round = player.group.in_round(1).exp_round + 1, task_number = 4, param_number = 5, disagreement_quantity = player.subsession.disagreement_q, ) @staticmethod def is_displayed(player: Player): return player.round_number == 1 class WP1_RoundStart_CwC(WaitPage): template_name = "consensus_chat/WP1_RoundStart_CwC.html" wait_for_all_groups = False @staticmethod def vars_for_template(player:Player): return dict( exp_round = player.group.exp_round ) @staticmethod def is_displayed(player: Player): return player.round_number <= C.NUM_ROUNDS class P2_Task_CwC(Page): form_model = 'player' form_fields = ['quantity', 'typeAPayoff', 'typeBPayoff', 'typeCPayoff'] def get_timeout_seconds(player): return C.TIME_TASK @staticmethod def before_next_page(player, timeout_happened): """ Does calculations after the player finishes group decision-making for the round, before the next round starts Args: player (Player): The player accessing the page timeout_happened (bool): Whether the player ran out of time on the page """ # check if no one in the group submitted other = player.get_others_in_group() other_confirmed = [op.quantityTime for op in other] max_confirmed = max(other_confirmed) # print(player.group.exp_round, player.group.task_number, timeout_happened, other_confirmed, max_confirmed) if (timeout_happened): if (player.quantityTime != -1) & (max_confirmed != -1): # print("But I already confirmed") player.timeout_task = 0 elif (player.quantityTime == -1) & (max_confirmed != -1): player.timeout_task = 1 player.quantity = np.nan player.typeAPayoff = player.group.dis_typeAPayoff player.typeBPayoff = player.group.dis_typeBPayoff player.typeCPayoff = player.group.dis_typeCPayoff elif (player.quantityTime == -1) & (max_confirmed == -1): player.timeout_task = 2 player.quantity = np.nan player.typeAPayoff = player.group.dis_typeAPayoff player.typeBPayoff = player.group.dis_typeBPayoff player.typeCPayoff = player.group.dis_typeCPayoff # print(player.timeout_task) quantity = player.quantity if player.timeout_task == 1: quantity = "Not Submitted" elif player.timeout_task == 2: quantity = "Not Submitted" # print(quantity) if player.timeout_task != 2: player.participant.vars["roundHistory"].append(player.group.exp_round) player.participant.vars["taskHistory"].append(player.group.task_number) player.participant.vars["paramHistory"].append(player.group.param_number) player.participant.vars["quantityHistory"].append(quantity) player.participant.vars["typeAPayoffHistory"].append(player.typeAPayoff) player.participant.vars["typeBPayoffHistory"].append(player.typeBPayoff) player.participant.vars["typeCPayoffHistory"].append(player.typeCPayoff) player.participant.vars["myIDinGroup"].append(player.id_in_group) player.participant.vars["otherID"].append(player.otherid) player.participant.vars["myGroupID"].append(player.group_pid) player.participant.vars["appHistory"].append("CwC") print("Testing History, Type A = ", player.participant.vars["typeAPayoffHistory"]) @staticmethod def vars_for_template(player:Player): player.starttime = time.time() # print("Start Time:", player.starttime) # print(player.participant.vars["roundHistory"], # player.participant.vars["taskHistory"], # player.participant.vars["paramHistory"], # player.participant.vars["quantityHistory"], # player.participant.vars["typeAPayoffHistory"], # player.participant.vars["typeBPayoffHistory"], # player.participant.vars["typeCPayoffHistory"], # player.participant.vars["myIDinGroup"], # player.participant.vars["otherID"], # player.participant.vars["myGroupID"], # player.participant.vars["appHistory"]) return dict( exp_round = player.group.exp_round, mytype = player.mytype, othertype = player.group.get_player_by_id(player.otherid).mytype, task_number = player.group.task_number, time_individual_task = C.TIME_TASK, ) @staticmethod def js_vars(player): if(player.session.config['line_input'] != "None"): equations = json.loads(player.session.config['line_input']) else: equations = player.session.config['line_dict'] return dict( equations=equations, colors=player.session.config['graph_colors'], mytype=player.mytype, num_parameter_sets=len(equations), exp_round = player.group.exp_round, task_number = player.group.task_number, param_number = player.group.param_number, time_group_task = C.TIME_TASK, ) @staticmethod def live_method(player: Player, data): # print("Backend", data) if data['action_app'] == 'Chat': # print("Chat") text = data['text'] ret_dict_me = dict( action_app = 'Chat', nickname = "Type " + player.mytype, me = 1, text = text, group = player.group.id_in_subsession, player = player.id_in_group, ) ret_dict_other = dict( action_app = 'Chat', nickname = "Type " + player.mytype, me = 0, text = text, group = player.group.id_in_subsession, player = player.id_in_group, ) Message.create( session_code = player.session.code, sender_participant_code = player.participant.code, exp_round = player.group.exp_round, task_number = player.group.task_number, group_id = player.group.id_in_subsession, sender_id_group = player.id_in_group, partner_id_group = player.otherid, time_remaining = data['timeRemaining'], sender_nickname = "Type " + player.mytype, sender_confirmed = 1 if (player.quantityTime != -1) else 0, text=text ) return {player.id_in_group: [ret_dict_me], player.otherid: [ret_dict_other]} elif data["action_app"] == "My Proposal": # print("Updated My Proposal") player.myProposalTimeTaskHistory += ","+str(data["timeRemaining"]) player.myQuantityTaskHistory += ","+str(data["quantity"]) player.myTypeAPayoffTaskHistory += ","+str(round(data["A_payoff"],2)) player.myTypeBPayoffTaskHistory += ","+str(round(data["B_payoff"],2)) player.myTypeCPayoffTaskHistory += ","+str(round(data["C_payoff"],2)) # Sending proposal to Other ret_dict_other = dict( action_app = 'Others_offer', otherQuantity = float(data["quantity"]), ) return {player.otherid: [ret_dict_other]} elif data["action_app"] == "Other Proposal": # No return message necessary # print("Updated Other Proposal") player.otherProposalTimeTaskHistory += ","+str(data["timeRemaining"]) player.otherQuantityTaskHistory += ","+str(data["quantity"]) player.otherTypeAPayoffTaskHistory += ","+str(round(data["A_payoff"],2)) player.otherTypeBPayoffTaskHistory += ","+str(round(data["B_payoff"],2)) player.otherTypeCPayoffTaskHistory += ","+str(round(data["C_payoff"],2)) # print(player.otherQuantityTaskHistory) elif data["action_app"] == "My Cancel": player.myCancelTimeTaskHistory += ","+str(data["timeRemaining"]) return { player.otherid: [dict(action_app = 'Others_offer_cancel')] } elif data["action_app"] == "Other Cancel": # No return message necessary player.otherCancelTimeTaskHistory += ","+str(data["timeRemaining"]) elif data["action_app"] == "My Confirm": # print("I confirmed") player.quantityTime = int(data["timeRemaining"]) player.group.confirmed += 2 # One person confirming meaning both confirm player.myProposalTimeTaskHistory += ","+str(data["timeRemaining"]) player.myQuantityTaskHistory += ","+str(data["quantity"]) player.myTypeAPayoffTaskHistory += ","+str(round(data["A_payoff"],2)) player.myTypeBPayoffTaskHistory += ","+str(round(data["B_payoff"],2)) player.myTypeCPayoffTaskHistory += ","+str(round(data["C_payoff"],2)) # print(player.group.confirmed) # print("Asking Other to Confirm") return { player.otherid: [dict(action_app = 'Other_Confirmed')] } elif data["action_app"] == "Other Confirm": # print("I have to confirm now because my partner has confirmed") player.quantityTime = int(data["timeRemaining"]) player.otherProposalTimeTaskHistory += ","+str(data["timeRemaining"]) player.otherQuantityTaskHistory += ","+str(data["quantity"]) player.otherTypeAPayoffTaskHistory += ","+str(round(data["A_payoff"],2)) player.otherTypeBPayoffTaskHistory += ","+str(round(data["B_payoff"],2)) player.otherTypeCPayoffTaskHistory += ","+str(round(data["C_payoff"],2)) if player.group.confirmed == 2*C.NUM_TYPES: # print("Submitting every ones") all_players = player.group.get_players() all_but_partner_msg = [dict(action_app = 'Confirm', finished=True)] ret_dict = {} for p in all_players: ret_dict[p.id_in_group] = all_but_partner_msg # print(ret_dict) return ret_dict elif data["action_app"] == "Refresh": # print("Refresh") # print(player.myQuantityTaskHistory) ret_list = [] myActive = 0 otherActive = 0 my_last_proposal_time = -1 other_last_proposal_time = -1 my_last_cancel_time = C.TIME_TASK + 10 other_last_cancel_time = C.TIME_TASK + 10 mySliderValue = 0 otherSliderValue = 0 # print("My Proposal Time:", my_last_proposal_time) # print("My Cancel Time:", my_last_cancel_time) # print("Other Proposal Time:", other_last_proposal_time) # print("Other Cancel Time:", other_last_cancel_time) if (player.myProposalTimeTaskHistory != ""): my_last_proposal_time = float(player.myProposalTimeTaskHistory.split(",")[-1].strip()) if (player.otherProposalTimeTaskHistory != ""): other_last_proposal_time = float(player.otherProposalTimeTaskHistory.split(",")[-1].strip()) if (player.myCancelTimeTaskHistory != ""): my_last_cancel_time = float(player.myCancelTimeTaskHistory.split(",")[-1].strip()) if (player.otherCancelTimeTaskHistory != ""): other_last_cancel_time = float(player.otherCancelTimeTaskHistory.split(",")[-1].strip()) if (my_last_proposal_time < my_last_cancel_time) & (my_last_proposal_time != -1): myActive = 1 mySliderValue = float(player.myQuantityTaskHistory.split(",")[-1].strip()) if (other_last_proposal_time < other_last_cancel_time) & (other_last_proposal_time != -1): otherActive = 1 # print("Hello", player.otherQuantityTaskHistory.split(",")[-1].strip()) otherSliderValue = float(player.otherQuantityTaskHistory.split(",")[-1].strip()) # print("My Proposal Time:", my_last_proposal_time) # print("My Cancel Time:", my_last_cancel_time) # print("Other Proposal Time:", other_last_proposal_time) # print("Other Cancel Time:", other_last_cancel_time) ret_list.append(dict( action_app = 'Confirm_Refresh', confirmed = True if player.quantityTime != -1 else False, my_active = myActive, other_active = otherActive, my_slider_value = mySliderValue, other_slider_value = otherSliderValue, )) for msg in Message.filter(): if (msg.session_code == player.session.code) & (msg.exp_round == player.group.exp_round) & (msg.task_number == player.group.task_number) & (msg.group_id == player.group.id_in_subsession): if player.id_in_group == msg.sender_id_group: ret_list.append(to_dict(msg, me=1)) elif player.id_in_group == msg.partner_id_group: ret_list.append(to_dict(msg, me=0)) # print("What should be returned: ", ret_list) return {player.id_in_group: ret_list} @staticmethod def is_displayed(player: Player): return player.round_number <= C.NUM_ROUNDS class WP2_TaskEnd_CwC(WaitPage): template_name = "consensus_chat/WP2_TaskEnd_CwC.html" wait_for_all_groups = False @staticmethod def vars_for_template(player:Player): return dict( exp_round = player.group.exp_round ) @staticmethod def is_displayed(player: Player): return player.round_number <= C.NUM_ROUNDS class P3_TaskResult_CwC(Page): @staticmethod def vars_for_template(player:Player): if player.timeout_task == 1: quantity = "Not Submitted" elif player.timeout_task == 0: quantity = player.quantity else: quantity = "Not Submitted" lasttr=0 if player.round_number == C.NUM_ROUNDS: lasttr = 1 lasttask=0 if player.group.task_number == C.NUM_PARAMS: lasttask = 1 return dict( exp_round = player.group.exp_round, task_number = player.group.task_number, lasttr = lasttr, lasttask = lasttask, mytype = player.mytype, othertype = player.group.get_player_by_id(player.otherid).mytype, quantity = quantity, typeAPayoff = np.round(player.typeAPayoff,2), typeBPayoff = np.round(player.typeBPayoff,2), typeCPayoff = np.round(player.typeCPayoff,2), ) @staticmethod def is_displayed(player: Player): # print("End of App History") # print(player.participant.vars["roundHistory"], # player.participant.vars["taskHistory"], # player.participant.vars["paramHistory"], # player.participant.vars["quantityHistory"], # player.participant.vars["typeAPayoffHistory"], # player.participant.vars["typeBPayoffHistory"], # player.participant.vars["typeCPayoffHistory"], # player.participant.vars["myIDinGroup"], # player.participant.vars["otherID"], # player.participant.vars["myGroupID"], # player.participant.vars["appHistory"]) return player.round_number <= C.NUM_ROUNDS page_sequence = [ P1_Intro_CwC, WP1_RoundStart_CwC, P2_Task_CwC, WP2_TaskEnd_CwC, P3_TaskResult_CwC, ] def custom_export(players): yield ["session_code","exp_round","task_number","group_id","sender_id_group","partner_id_group","sender_participant_code","time_remaining","sender_nickname","sender_confirmed","text"] # 'filter' without any args returns everything messages = Message.filter() for msg in messages: yield([ msg.session_code, msg.exp_round, msg.task_number, msg.group_id, msg.sender_id_group, msg.partner_id_group, msg.sender_participant_code, msg.time_remaining, msg.sender_nickname, msg.sender_confirmed, msg.text, ])