from otree.api import * import random doc = """ general code for the binary majority-rule coordination game """ class C(BaseConstants): NAME_IN_URL = 'bmcg' PLAYERS_PER_GROUP = 5 NUM_ROUNDS = 15 # cents scaling (consistent with btpgg/btcgg) BENEFIT = cu(150) # benefit to each player on the winning side ACTION_ONE = cu(75) # cost if realized action = 1 (A) ACTION_ZERO = cu(30) # cost if realized action = 0 (B) ENDOWMENT = cu(80) # optional: keep payoffs positive (same as your others) class Subsession(BaseSubsession): def creating_session(self): if self.round_number == 1 and 'paying_round' not in self.session.vars: self.session.vars['paying_round'] = random.randint(1, C.NUM_ROUNDS) class Group(BaseGroup): num_action_one = models.IntegerField() num_action_zero = models.IntegerField() winning_side = models.IntegerField(choices=[0, 1]) is_tie = models.BooleanField() class Player(BasePlayer): # Chat gate (same pattern) ready_to_decide = models.BooleanField(initial=False) # Slider: probability of playing 1 (A) mix_prob = models.FloatField(min=0, max=1) # Realized action after Bernoulli draw realized_action = models.IntegerField(choices=[0, 1]) # in-case group.id_in_subsession fails to save again group_number = models.IntegerField() # for the merge with chat data later chat_channel_label = models.StringField() # ---------- helpers ---------- def get_endowment(obj): val = obj.session.config.get('ENDOWMENT', float(C.ENDOWMENT)) return cu(val) def get_benefit(obj): val = obj.session.config.get('BENEFIT', float(C.BENEFIT)) return cu(val) def get_action_one(obj): val = obj.session.config.get('ACTION_ONE', float(C.ACTION_ONE)) return cu(val) def get_action_zero(obj): val = obj.session.config.get('ACTION_ZERO', float(C.ACTION_ZERO)) return cu(val) # ---------- FUNCTIONS ---------- def draw_actions_and_set_payoffs(group: Group): players = group.get_players() # realize actions for p in players: p.realized_action = 1 if random.random() < p.mix_prob else 0 group.num_action_one = sum(p.realized_action for p in players) group.num_action_zero = len(players) - group.num_action_one # decide winner: larger side wins; tie -> fair coin if group.num_action_one > group.num_action_zero: group.winning_side = 1 group.is_tie = False elif group.num_action_zero > group.num_action_one: group.winning_side = 0 group.is_tie = False else: group.is_tie = True group.winning_side = random.choice([0, 1]) endowment = get_endowment(group) benefit = get_benefit(group) c1 = get_action_one(group) c0 = get_action_zero(group) for p in players: cost = c1 if p.realized_action == 1 else c0 win_bonus = benefit if p.realized_action == group.winning_side else cu(0) # consistent with btpgg/btcgg: include endowment p.payoff = endowment + (win_bonus - cost) def creating_session(subsession: Subsession): subsession.group_randomly() for g in subsession.get_groups(): for p in g.get_players(): p.group_number = g.id_in_subsession p.chat_channel_label = f"{subsession.round_number}--{g.id_in_subsession}" # ---------- PAGES ---------- class Round(Page): @staticmethod def vars_for_template(player: Player): return dict(cur_round=player.round_number, num_rounds=C.NUM_ROUNDS) class MyWaitPagePre(WaitPage): template_name = 'bmcg/MyWaitPagePre.html' class Chat(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): return dict( cur_round=player.round_number, num_rounds=C.NUM_ROUNDS, players_per_group=C.PLAYERS_PER_GROUP, benefit=get_benefit(player), cost_one=get_action_one(player), cost_zero=get_action_zero(player), endowment=get_endowment(player), chat_channel=f"{player.round_number}--{player.group_number}", ) @staticmethod def live_method(player: Player, data): """ Frontend sends: { "type": "ping" } -> return current count to just this player { "type": "ready" } -> mark this player ready + broadcast new count; if all ready, broadcast "advance" """ group = player.group def ready_count(): return sum(bool(p.participant.vars.get('ready_to_decide')) for p in group.get_players()) msg_type = data.get("type") if msg_type == "ping": return {player.id_in_group: {"ready_count": ready_count(), "total": C.PLAYERS_PER_GROUP}} if msg_type == "ready": if not player.participant.vars.get('ready_to_decide'): player.participant.vars['ready_to_decide'] = True rc = ready_count() payload = {"ready_count": rc, "total": C.PLAYERS_PER_GROUP} if rc >= C.PLAYERS_PER_GROUP: payload["advance"] = True return {0: payload} return {} class Decision(Page): form_model = 'player' form_fields = ['mix_prob'] @staticmethod def vars_for_template(player: Player): return dict( cur_round=player.round_number, num_rounds=C.NUM_ROUNDS, players_per_group=C.PLAYERS_PER_GROUP, benefit=get_benefit(player), cost_one=get_action_one(player), cost_zero=get_action_zero(player), endowment=get_endowment(player), chat_channel=f"{player.round_number}--{player.group_number}", ) @staticmethod def is_displayed(player: Player): # clear flag for future rounds (match your other apps) player.participant.vars.pop('ready_to_decide', None) return True class MyWaitPage(WaitPage): template_name = 'bmcg/MyWaitPage.html' after_all_players_arrive = draw_actions_and_set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): g = player.group return dict( cur_round=player.round_number, num_rounds=C.NUM_ROUNDS, your_p=int(100 * round(player.mix_prob, 2)), your_action="A" if player.realized_action == 1 else "B", your_action_bool=player.realized_action, num_ones=g.num_action_one, num_zeros=g.num_action_zero, is_tie=g.is_tie, winner=g.winning_side, # 1 means A-majority, 0 means B-majority benefit=get_benefit(player), cost_one=get_action_one(player), cost_zero=get_action_zero(player), endowment=get_endowment(player), ) class Payments(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): pr = player.session.vars.get('paying_round') if pr is None: pr = random.randint(1, C.NUM_ROUNDS) player.session.vars['paying_round'] = pr paying = player.in_round(pr).payoff # ---- NEW: quiz bonus from round 1 ---- quiz_count = player.participant.vars['cq_first_correct_count'] quiz_bonus = player.participant.vars['cq_bonus'] total = paying + quiz_bonus return dict( paying_round=pr, paying_round_payoff=paying, quiz_correct_count=quiz_count, quiz_bonus_amount=quiz_bonus, total_payoff_display=total, ) @staticmethod def before_next_page(player: Player, timeout_happened): pr = player.session.vars['paying_round'] quiz_bonus = player.participant.vars.get('cq_bonus', cu(0)) player.participant.payoff = player.in_round(pr).payoff + quiz_bonus page_sequence = [Round, MyWaitPagePre, Chat, Decision, MyWaitPage, Results, Payments]