from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range ) import random import time from identity_priming.models import C as IdentityC from identity_priming.models import choice_payoff author = 'Huanren Zhang' doc = """ Payment information that include eet """ class Constants(BaseConstants): name_in_url = 'payment' players_per_group = None num_rounds = 1 CHOICE_WAIT_TIMEOUT_SEC = 150 class Subsession(BaseSubsession): def group_by_arrival_time_method(self, waiting_players): # Build a fast lookup from payment-player id to the identity_priming state # previously stored in participant.vars. player_state = {} for player in self.get_players(): state = player.participant.vars.get('identity_state') if state: player_state[player.id_in_subsession] = state def _max_total_for_treatment(treatment): stats = self.session.vars.get('identity_choice_stats', {}) if treatment == 'baseline': return int(stats.get('baseline', {}).get('all', {}).get('total', 0)) tstats = stats.get(treatment, {}) max_total = 0 for identity_stats in tstats.values(): total = int(identity_stats.get('same', {}).get('total', 0)) if total > max_total: max_total = total return max_total def _resolve_imagined_choice(player, state): payoff = 0 imagined_identity = 'Baseline' if state and state.get('qualified', True): treatment = state.get('treatment') rows = state.get('rows', []) selected_row = None if treatment == 'gender': imagined_identity = random.choice(IdentityC.GENDERS) same_group = bool(state.get('identity') == imagined_identity) selected_row = next((r for r in rows if r.get('same_group') == same_group), None) elif treatment == 'ethnicity': imagined_identity = random.choice(IdentityC.ETHNICITIES) same_group = bool(state.get('identity') == imagined_identity) selected_row = next((r for r in rows if r.get('same_group') == same_group), None) else: selected_row = rows[0] if rows else None if selected_row and selected_row.get('choice') in IdentityC.CHOICES: payoff = choice_payoff(selected_row.get('choice'), 'B') player.participant.vars['identity_payoff_source'] = 'choice' player.participant.vars['identity_matched_partner_id'] = 'imagined' player.participant.vars['identity_imagined_partner_identity'] = imagined_identity player.participant.vars['identity_priming_payoff'] = payoff player.payoff = payoff player.payment_source = 'choice' player.matched_identity = imagined_identity player.matched_participant_id = 'imagined' player.actual_choice_number = None player.guessed_choice_number = None player.participant.vars.pop('choice_wait_started_at', None) player.participant.vars['ip_source_assigned'] = True player.participant.vars['ip_payoff_done'] = True def _resolve_belief_payoff(player, state): player.participant.vars['identity_payoff_source'] = 'belief' rows = state.get('rows', []) rows_with_belief = [r for r in rows if r.get('belief') is not None] if not rows_with_belief: player.participant.vars['identity_priming_payoff'] = 0 player.payoff = 0 player.payment_source = 'belief' player.matched_identity = '' player.matched_participant_id = '' player.actual_choice_number = 0 player.guessed_choice_number = None player.participant.vars['ip_payoff_done'] = True player.participant.vars.pop('choice_wait_started_at', None) return selected = random.choice(rows_with_belief) treatment = state.get('treatment') stats = self.session.vars.get('identity_choice_stats', {}) a_count = 0 b_count = 0 target_identity = 'Baseline' if treatment == 'baseline': bucket = stats.get('baseline', {}).get('all', {}) a_count = int(bucket.get('A', 0)) b_count = int(bucket.get('B', 0)) elif treatment in ('gender', 'ethnicity'): tstats = stats.get(treatment, {}) identities = list(IdentityC.GENDERS) if treatment == 'gender' else list(IdentityC.ETHNICITIES) player_identity = state.get('identity') same_group = bool(selected.get('same_group')) if player_identity in identities: if same_group: opponent_identity = player_identity else: opponent_identity = next((identity for identity in identities if identity != player_identity), None) else: opponent_identity = identities[0] if identities else None if opponent_identity in identities and len(identities) == 2: other_identity = identities[1] if identities[0] == opponent_identity else identities[0] if int(tstats.get(opponent_identity, {}).get('same', {}).get('total', 0)) < 10: opponent_identity = other_identity if opponent_identity: target_identity = opponent_identity target_relation = 'same' if player_identity and opponent_identity == player_identity else 'diff' bucket = tstats.get(opponent_identity, {}).get(target_relation, {}) a_count = int(bucket.get('A', 0)) b_count = int(bucket.get('B', 0)) total_count = a_count + b_count if total_count <= 0: player.participant.vars['identity_priming_payoff'] = 0 player.payoff = 0 player.payment_source = 'belief' player.matched_identity = target_identity player.matched_participant_id = '' player.actual_choice_number = 0 player.guessed_choice_number = int(selected.get('belief')) player.participant.vars['ip_payoff_done'] = True player.participant.vars.pop('choice_wait_started_at', None) return if total_count < 10: draws = random.choices(['A', 'B'], weights=[a_count, b_count], k=10) x_count = sum(1 for d in draws if d == 'A') else: remaining_a = a_count remaining_b = b_count x_count = 0 for _ in range(10): remaining_total = remaining_a + remaining_b if remaining_total <= 0: break if random.random() * remaining_total < remaining_a: x_count += 1 remaining_a -= 1 else: remaining_b -= 1 diff = abs(int(selected.get('belief')) - x_count) payoff = IdentityC.BELIEF_PAYOFFS.get(diff, 0) player.participant.vars['identity_priming_payoff'] = payoff player.payoff = payoff player.payment_source = 'belief' player.matched_identity = target_identity player.matched_participant_id = '' player.actual_choice_number = x_count player.guessed_choice_number = int(selected.get('belief')) player.participant.vars['ip_payoff_done'] = True player.participant.vars.pop('choice_wait_started_at', None) print('waiting_players',waiting_players) # print('player_state', player_state) # Phase 1: assign each newly waiting participant a payoff source # ('choice' or 'belief') exactly once. for player in waiting_players: state = player_state.get(player.id_in_subsession) # Manual override: allow forcing a one-player imagined match from the # wait page for operational recovery/debug. if player.participant.vars.get('ip_force_imagined', False): player.participant.vars['ip_force_imagined'] = False _resolve_imagined_choice(player, state) return [player] # No state means no identity_priming data is available; mark done so # the participant is not stuck waiting. if not state or not state.get('qualified', True): player.participant.vars['identity_priming_payoff'] = 0 player.payoff = 0 player.payment_source = '' player.matched_identity = '' player.matched_participant_id = '' player.actual_choice_number = None player.guessed_choice_number = None player.participant.vars.pop('choice_wait_started_at', None) player.participant.vars['ip_payoff_done'] = True player.participant.vars['ip_source_assigned'] = True continue # Keep the previous assignment when this method is called again. if player.participant.vars.get('ip_source_assigned'): if ( player.participant.vars.get('identity_payoff_source') == 'choice' and not player.participant.vars.get('choice_wait_started_at') ): player.participant.vars['choice_wait_started_at'] = time.time() continue # Incomplete choices cannot be scored; assign zero and release. if not state.get('choices_complete', False): player.participant.vars['identity_payoff_source'] = 'choice' player.participant.vars['identity_priming_payoff'] = 0 player.payoff = 0 player.payment_source = 'choice' player.matched_identity = '' player.matched_participant_id = '' player.actual_choice_number = None player.guessed_choice_number = None player.participant.vars.pop('choice_wait_started_at', None) player.participant.vars['ip_payoff_done'] = True player.participant.vars['ip_source_assigned'] = True continue # Apply the source rule using treatment-level choice counts from # session.vars['identity_choice_stats']. treatment = state.get('treatment') max_total = _max_total_for_treatment(treatment) print(treatment, 'max_total' , max_total) if max_total < 10: # Low sample size: always use choice payoff. source = 'choice' else: # Otherwise randomize source; partner availability is handled by # fallback logic later (timeout or no-active-partner fallback). source = random.choice(['choice', 'belief']) player.participant.vars['identity_payoff_source'] = source player.payment_source = source if source == 'choice': player.participant.vars.setdefault('choice_wait_started_at', time.time()) else: player.participant.vars.pop('choice_wait_started_at', None) player.participant.vars['ip_source_assigned'] = True # Phase 2: immediately release players that were already finalized above. for player in waiting_players: if player.participant.vars.get('ip_payoff_done'): print('Finalized before:',player) return [player] # Phase 3: belief-source players can be scored individually (no match needed). # # For each waiting player assigned to source='belief': # 1) pick one of that player's submitted belief rows at random; # 2) construct an empirical A/B distribution from session-level choice stats # for the relevant treatment/identity bucket; # 3) simulate 10 opponent choices from that distribution to get "actual X"; # 4) score belief accuracy using IdentityC.BELIEF_PAYOFFS; # 5) persist both payoff and diagnostics, then release the player. for player in waiting_players: state = player_state.get(player.id_in_subsession) if state and player.participant.vars.get('identity_payoff_source') == 'belief': _resolve_belief_payoff(player, state) print('Belief payment:',player) return [player] # Phase 4: choice-source players wait until a same-treatment partner exists. # Match by waiting order and score both players in the pair together. for i, player1 in enumerate(waiting_players): state1 = player_state.get(player1.id_in_subsession) if ( not state1 or not state1.get('qualified', True) or player1.participant.vars.get('identity_payoff_source') != 'choice' or not state1.get('choices_complete', False) ): continue for j in range(i + 1, len(waiting_players)): player2 = waiting_players[j] state2 = player_state.get(player2.id_in_subsession) if ( not state2 or not state2.get('qualified', True) or player2.participant.vars.get('identity_payoff_source') != 'choice' or not state2.get('choices_complete', False) ): continue if state1.get('treatment') != state2.get('treatment'): continue # Compute choice payoff directly for this matched pair. player1.participant.vars['identity_matched_partner_id'] = player2.participant.id_in_session player2.participant.vars['identity_matched_partner_id'] = player1.participant.id_in_session treatment = state1.get('treatment') rows1 = state1.get('rows', []) rows2 = state2.get('rows', []) if treatment == 'baseline': row1 = rows1[0] if rows1 else None row2 = rows2[0] if rows2 else None else: identity1 = state1.get('identity') identity2 = state2.get('identity') same_group = bool(identity1 and identity2 and identity1 == identity2) row1 = next((r for r in rows1 if r.get('same_group') == same_group), None) row2 = next((r for r in rows2 if r.get('same_group') == same_group), None) if not row1 or not row2 or not row1.get('choice') or not row2.get('choice'): payoff1 = 0 payoff2 = 0 else: payoff1 = choice_payoff(row1.get('choice'), row2.get('choice')) payoff2 = choice_payoff(row2.get('choice'), row1.get('choice')) player1.participant.vars['identity_priming_payoff'] = payoff1 player2.participant.vars['identity_priming_payoff'] = payoff2 player1.payoff = payoff1 player2.payoff = payoff2 player1.payment_source = 'choice' player2.payment_source = 'choice' player1.matched_identity = state2.get('identity') or 'Baseline' player2.matched_identity = state1.get('identity') or 'Baseline' player1.matched_participant_id = str(player2.participant.id_in_session) player2.matched_participant_id = str(player1.participant.id_in_session) player1.actual_choice_number = None player2.actual_choice_number = None player1.guessed_choice_number = None player2.guessed_choice_number = None player1.participant.vars.pop('choice_wait_started_at', None) player2.participant.vars.pop('choice_wait_started_at', None) player1.participant.vars['ip_payoff_done'] = True player2.participant.vars['ip_payoff_done'] = True print('Choice payment:',player1, player2) return [player1, player2] # Phase 5: fallback for unmatched choice-source players. # Fallback is triggered in either case: # - no active same-treatment partner exists right now, or # - waiting time exceeds CHOICE_WAIT_TIMEOUT_SEC. # # When fallback is triggered: # - if treatment sample size allows belief (max_total >= 10), use belief; # - otherwise use an imagined choice partner. now = time.time() for player in waiting_players: state = player_state.get(player.id_in_subsession) if ( not state or not state.get('qualified', True) or player.participant.vars.get('identity_payoff_source') != 'choice' or not state.get('choices_complete', False) ): continue treatment = state.get('treatment') has_active_partner = any( other != player and not other.participant.vars.get('ip_payoff_done', False) and other.participant.vars.get('qualified', True) and other.participant.vars.get('treatment') == treatment for other in self.get_players() ) if not has_active_partner: no_partner_max_total = _max_total_for_treatment(treatment) if no_partner_max_total >= 10: _resolve_belief_payoff(player, state) player.participant.vars['choice_timeout_fallback'] = 'no_partner_belief' print('No active partner -> belief:', player) return [player] _resolve_imagined_choice(player, state) player.participant.vars['choice_timeout_fallback'] = 'no_partner_imagined' print('No active partner -> imagined:', player) return [player] waited_sec = now - float(player.participant.vars.get('choice_wait_started_at')) print('player',player.id_in_subsession, waited_sec) if waited_sec < Constants.CHOICE_WAIT_TIMEOUT_SEC: continue timeout_max_total = _max_total_for_treatment(treatment) if timeout_max_total >= 10: _resolve_belief_payoff(player, state) player.participant.vars['choice_timeout_fallback'] = 'belief' print('Choice timeout -> belief:', player) return [player] _resolve_imagined_choice(player, state) player.participant.vars['choice_timeout_fallback'] = 'imagined' print('Choice timeout -> imagined:', player) return [player] return None class Group(BaseGroup): pass class Player(BasePlayer): experiment_duration = models.FloatField(initial=0) qualified = models.BooleanField(initial=False) submitted = models.BooleanField(initial=False) final_payment = models.FloatField(initial=0) bonus = models.FloatField(initial=0) payoff_part12 = models.IntegerField(initial=0) payoff_part3 = models.IntegerField(initial=0) payment_source = models.StringField(blank=True, initial='') matched_identity = models.StringField(blank=True, initial='') matched_participant_id = models.StringField(blank=True, initial='') actual_choice_number = models.IntegerField(blank=True) guessed_choice_number = models.IntegerField(blank=True) def live_mark_submitted(self, data): if data.get('type') == 'submitted': self.submitted = True self.participant.vars['submitted'] = True return {self.id_in_group: dict(type='submitted_ack')} return {self.id_in_group: dict(type='noop')}