import time from otree.api import * from constants import * import json from collections import defaultdict doc = """Joint Task (Spot the Difference) for Generations 1 and 2""" class C(BaseConstants): NAME_IN_URL = 'F_JointTask_Gen12_D' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 def _ensure_caches(session, subsession=None): """Caches are built at session creation in AA_SetUpApp. This function is now just a safety check — no DB scan ever occurs.""" if session.vars.get('_caches_built') and session.vars.get('gen2_count', 0) > 0: return # Already built — instant return # Only reaches here if something went wrong at session creation import logging logging.warning("Cache was not pre-built at session creation — check AA_SetUpApp creating_session") # ── Subsession: creating_session REMOVED ────────────────────────────────────── # The cache-resetting creating_session that previously lived here has been # removed. AA_SetUpApp.creating_session now builds all caches once at session # creation. Resetting them here would destroy that work before any page loads. class Subsession(BaseSubsession): pass def _get_fam_state(session, fam, round_id, board_len=15): if 'spotdiff_state' not in session.vars: session.vars['spotdiff_state'] = {} state = session.vars['spotdiff_state'] key = f"{fam}_{round_id}" if key not in state: state[key] = {'board': [-1] * board_len, 'click_count': 0, 'whose_turn': 1} return state[key] def _get_fam_members(session, fam, subsession=None): """Pure cache lookup — no DB scan ever. Family caches are populated during Selection_G12 as assignments are made.""" return session.vars.get(f"fam_members_{fam}", []) class Group(BaseGroup): board_state = models.LongStringField(initial=json.dumps([-1] * 18)) class Player(BasePlayer): Ana_generation = models.IntegerField() Ana_own_id = models.IntegerField() Ana_family_assignment = models.StringField() parent_id = models.IntegerField(initial=-100) grandparent_id = models.IntegerField() child_id = models.IntegerField(initial=-100) joint_task_r1_score = models.IntegerField(blank=True, initial=0) joint_task_r2_score = models.IntegerField(blank=True, initial=0) joint_task_r3_score = models.IntegerField(blank=True, initial=0) class Introduction2(Page): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class WaitForIntroduction_G12(WaitPage): wait_for_all_groups = True @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class WaitForSelection_G12(WaitPage): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class Selection_G12(Page): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def js_vars(player: Player): return dict( my_symbol=player.participant.vars.get('symbol'), my_id=player.id_in_subsession, my_generation=player.participant.vars.get('Ana_generation'), my_own_id=player.participant.vars.get('Ana_own_id'), my_subsession_id=player.id_in_subsession, family_assignment=player.participant.vars.get('Ana_family_assignment'), ) @staticmethod def vars_for_template(player): _ensure_caches(player.session, player.subsession) return dict( active_players=player.session.vars.get('active_ids', []), card_range=range(1, player.subsession.session.vars.get('allowed_gen2', 0) + 1), my_own_id=player.participant.vars.get('Ana_own_id'), generation=player.participant.vars.get('Ana_generation'), child_id=player.child_id, parent_id=player.parent_id, family_assignment=player.participant.vars.get('Ana_family_assignment'), ) @staticmethod def live_method(player: Player, data: dict): _ensure_caches(player.session, player.subsession) group = player.group board = json.loads(group.board_state) broadcast = {} BLANK = -1 # ── WARMUP PING: fired on page load to pre-warm WebSocket + DB connection ── if not data: active_gen2_count = player.session.vars.get('allowed_gen2', 0) family_map = { str(i + 1): board[i] for i in range(len(board)) if board[i] != BLANK } game_end = sum(1 for x in board if x != BLANK) >= active_gen2_count return {player.id_in_subsession: { 'ready': True, 'board_state': board, 'family_map': family_map, 'game_end': game_end, }} if 'move' in data: if player.participant.vars.get('Ana_generation') != 1: return {player.id_in_subsession: {'error': 'Only Generation 1 can select'}} move = data['move'] if move < 1 or move > len(board): return {player.id_in_subsession: {'error': 'Invalid card index'}} if board[move - 1] != BLANK: return {player.id_in_subsession: { 'error': 'Card already taken', 'board_state': board, }} if player.child_id != -100: return {player.id_in_subsession: {'error': 'You have already selected a successor'}} player_family = player.participant.vars.get('Ana_family_assignment') board[move - 1] = player_family group.board_state = json.dumps(board) player.child_id = move # ── CHANGED: use pre-built own_id map instead of scanning all players ── lookup_key = f"gen2_{move}" gen2_subsession_id = player.session.vars.get( 'own_id_to_subsession_id', {} ).get(lookup_key) if gen2_subsession_id: gen2_player = next( (q for q in player.subsession.get_players() if q.id_in_subsession == gen2_subsession_id), None ) if gen2_player: gen2_player.Ana_family_assignment = player_family gen2_player.participant.vars['Ana_family_assignment'] = player_family gen2_player.parent_id = player.participant.vars.get('Ana_own_id') gen2_player.participant.vars['parent_id'] = player.participant.vars.get('Ana_own_id') for key in ['Ana_EndowmentType', 'Ana_Endowment', 'my_order', 'Ana_GroupID', 'Ana_EndowmentReason', 'Ana_RankingType', 'Ana_Mmb1_id', 'Ana_Mmb2_id', 'Ana_Mmb3_id', 'Ana_fname_Mmb1', 'Ana_fname_Mmb2', 'Ana_fname_Mmb3', 'Ana_Edmt_Mmb1', 'Ana_Edmt_Mmb2', 'Ana_Edmt_Mmb3']: gen2_player.participant.vars[key] = player.participant.vars.get(key) # Update family member cache with both Gen1 and newly assigned Gen2 cache_key = f"fam_members_{player_family}" existing = player.session.vars.get(cache_key, []) updated = list(set(existing + [player.id_in_subsession, gen2_subsession_id])) player.session.vars[cache_key] = updated # ── END CHANGED ──────────────────────────────────────────────────────── family_map = { str(i + 1): board[i] for i in range(len(board)) if board[i] != BLANK } active_gen2_count = player.session.vars.get('allowed_gen2', 0) game_end = sum(1 for x in board if x != BLANK) >= active_gen2_count broadcast.update(board_state=board, family_map=family_map, game_end=game_end) if game_end: broadcast['_page'] = 'next' all_ids = ( player.session.vars.get('gen1_ids', []) + player.session.vars.get('gen2_ids', []) ) if not all_ids: all_ids = [p.id_in_subsession for p in player.subsession.get_players()] return {pid: broadcast for pid in all_ids} class JointTask_Intro_G12(Page): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): return { 'Ana_generation': player.participant.vars.get('Ana_generation'), 'Ana_family_assignment': player.participant.vars.get('Ana_family_assignment'), } class WaitForStart1(WaitPage): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class JointTask_R1p1(Page): timeout_seconds = TIME_JOINTTASK_STG1 @staticmethod def is_displayed(player): if player.participant.vars.get('Ana_generation') == 2: fam = player.participant.vars.get('Ana_family_assignment') if not fam or fam == 'XX' or fam == 'Unassigned': return False return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r1p1_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() # ── FIX: fetch saved chat history so it survives page refresh ── history_key = f"{fam}_chat_r1" chat_history = player.session.vars.get(history_key, []) # ─────────────────────────────────────────────────────────────── # Set the shared start anchor only when the LAST member arrives. # Once set, never overwrite it — this is the single source of truth # for both players' timers. start_key = f"r1p1_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() return dict(nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R1", chat_history=chat_history) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r1p1_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG1 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG1 @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') # ── WARMUP PING: warms WebSocket channel for both sender and receiver ── if not data: member_ids = _get_fam_members(player.session, fam, player.subsession) # Broadcast to ALL members so both sides of the channel warm simultaneously return {pid: {'ready': True} for pid in member_ids} # ── END WARMUP ────────────────────────────────────────────────────────── if 'chat' in data: MAX_CHAT_HISTORY = 200 history_key = f"{fam}_chat_r1" if history_key not in player.session.vars: player.session.vars[history_key] = [] history = player.session.vars[history_key] if len(history) < MAX_CHAT_HISTORY: history.append({ 'sender': data.get('nickname', 'Player'), 'chat': data['chat'] }) member_ids = _get_fam_members(player.session, fam, player.subsession) broadcast = {'chat': data['chat'], 'sender': data.get('nickname', 'Player')} return {pid: broadcast for pid in member_ids} class JointTask_R1p2(Page): timeout_seconds = TIME_JOINTTASK_STG2 @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r1p2_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() start_key = f"r1p2_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() history_key = f"{fam}_chat_r1" chat_history = player.session.vars.get(history_key, []) return dict( nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R1", chat_history=chat_history ) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r1p2_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG2 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG2 @staticmethod def js_vars(player: Player): return dict(my_symbol=player.participant.vars.get('symbol')) @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') fam_state = _get_fam_state(player.session, fam, round_id='r1', board_len=15) board = fam_state['board'] member_ids = _get_fam_members(player.session, fam, player.subsession) if not data: broadcast = { 'ready': True, 'spotdiffR1': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False, } return {pid: broadcast for pid in member_ids} if 'move' in data and player.participant.vars.get('Ana_generation') == 2: move = data['move'] fam_state = _get_fam_state(player.session, fam, round_id='r1', board_len=15) board = fam_state['board'] clicked_count = sum(1 for x in board if x == 1) if board[move] == -1: if clicked_count < 5: board[move] = 1 else: board[move] = -1 player.session.vars['spotdiff_state'] = player.session.vars['spotdiff_state'] broadcast = { 'spotdiffR1': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False } return {pid: broadcast for pid in member_ids} @staticmethod def before_next_page(player: Player, timeout_happened=False): fam = player.participant.vars.get('Ana_family_assignment') if not fam or player.participant.vars.get('r1_score_saved'): return fam_state = _get_fam_state(player.session, fam, round_id='r1', board_len=15) board = fam_state['board'] CORRECT_SPOTS_R1 = [0, 3, 6, 11, 12] score = sum(1 for position in CORRECT_SPOTS_R1 if board[position] == 1) member_ids = _get_fam_members(player.session, fam, player.subsession) id_set = set(member_ids) for p in player.subsession.get_players(): if p.id_in_subsession in id_set: p.joint_task_r1_score = score p.participant.vars['r1_score_saved'] = True class WaitForStart2(WaitPage): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class JointTask_R2p1(Page): timeout_seconds = TIME_JOINTTASK_STG1 @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r2p1_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() history_key = f"{fam}_chat_r2" chat_history = player.session.vars.get(history_key, []) start_key = f"r2p1_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() return dict(nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R2", chat_history=chat_history) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r2p1_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG1 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG1 @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') if not data: return {player.id_in_subsession: {'ready': True}} if 'chat' in data: MAX_CHAT_HISTORY = 200 history_key = f"{fam}_chat_r2" if history_key not in player.session.vars: player.session.vars[history_key] = [] history = player.session.vars[history_key] if len(history) < MAX_CHAT_HISTORY: history.append({ 'sender': data.get('nickname', 'Player'), 'chat': data['chat'] }) member_ids = _get_fam_members(player.session, fam, player.subsession) broadcast = {'chat': data['chat'], 'sender': data.get('nickname', 'Player')} return {pid: broadcast for pid in member_ids} class JointTask_R2p2(Page): timeout_seconds = TIME_JOINTTASK_STG2 @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r2p2_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() start_key = f"r2p2_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() history_key = f"{fam}_chat_r2" chat_history = player.session.vars.get(history_key, []) return dict( nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R2", chat_history=chat_history ) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r2p2_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG2 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG2 @staticmethod def js_vars(player: Player): return dict(my_symbol=player.participant.vars.get('symbol')) @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') fam_state = _get_fam_state(player.session, fam, round_id='r2', board_len=15) board = fam_state['board'] member_ids = _get_fam_members(player.session, fam, player.subsession) if not data: broadcast = { 'ready': True, 'spotdiffR2': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False, } return {pid: broadcast for pid in member_ids} if 'move' in data and player.participant.vars.get('Ana_generation') == 2: move = data['move'] fam_state = _get_fam_state(player.session, fam, round_id='r2', board_len=15) board = fam_state['board'] clicked_count = sum(1 for x in board if x == 1) if board[move] == -1: if clicked_count < 5: board[move] = 1 else: board[move] = -1 player.session.vars['spotdiff_state'] = player.session.vars['spotdiff_state'] broadcast = { 'spotdiffR2': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False } return {pid: broadcast for pid in member_ids} @staticmethod def before_next_page(player: Player, timeout_happened=False): fam = player.participant.vars.get('Ana_family_assignment') if not fam or player.participant.vars.get('r2_score_saved'): return fam_state = _get_fam_state(player.session, fam, round_id='r2', board_len=15) board = fam_state['board'] CORRECT_SPOTS_R2 = [0, 1, 5, 12, 14] score = sum(1 for position in CORRECT_SPOTS_R2 if board[position] == 1) member_ids = _get_fam_members(player.session, fam, player.subsession) id_set = set(member_ids) for p in player.subsession.get_players(): if p.id_in_subsession in id_set: p.joint_task_r2_score = score p.participant.vars['r2_score_saved'] = True class WaitForStart3(WaitPage): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") class JointTask_R3p1(Page): timeout_seconds = TIME_JOINTTASK_STG1 @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r3p1_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() history_key = f"{fam}_chat_r3" chat_history = player.session.vars.get(history_key, []) start_key = f"r3p1_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() return dict(nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R3", chat_history=chat_history) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r3p1_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG1 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG1 @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') if not data: return {player.id_in_subsession: {'ready': True}} if 'chat' in data: MAX_CHAT_HISTORY = 200 history_key = f"{fam}_chat_r3" if history_key not in player.session.vars: player.session.vars[history_key] = [] history = player.session.vars[history_key] if len(history) < MAX_CHAT_HISTORY: history.append({ 'sender': data.get('nickname', 'Player'), 'chat': data['chat'] }) member_ids = _get_fam_members(player.session, fam, player.subsession) broadcast = {'chat': data['chat'], 'sender': data.get('nickname', 'Player')} return {pid: broadcast for pid in member_ids} class JointTask_R3p2(Page): timeout_seconds = TIME_JOINTTASK_STG2 @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') in [1, 2] and player.participant.vars.get('state') == "active") @staticmethod def vars_for_template(player): from utils import chat_nickname fam = player.participant.vars.get('Ana_family_assignment') # Record arrival time for this player arrival_key = f"r3p2_arrived_{fam}" if arrival_key not in player.session.vars: player.session.vars[arrival_key] = {} player.session.vars[arrival_key][player.id_in_subsession] = time.time() start_key = f"r3p2_start_{fam}" member_ids = _get_fam_members(player.session, fam) arrivals = player.session.vars[arrival_key] if start_key not in player.session.vars and len(arrivals) >= len(member_ids) and member_ids: player.session.vars[start_key] = time.time() history_key = f"{fam}_chat_r3" chat_history = player.session.vars.get(history_key, []) return dict( nickname=chat_nickname(player), chat_channel=f"{fam}_G12_R3", chat_history=chat_history ) @staticmethod def get_timeout_seconds(player): fam = player.participant.vars.get('Ana_family_assignment') start_key = f"r3p2_start_{fam}" start_time = player.session.vars.get(start_key) if start_time: # Both players have arrived — compute remaining time from shared anchor elapsed = time.time() - start_time return max(TIME_JOINTTASK_STG2 - elapsed, 5) # Start anchor not set yet (second player hasn't loaded) — give full time. # oTree will use this value but the second player's arrival will set the # anchor, and their get_timeout_seconds will return the correct reduced time. return TIME_JOINTTASK_STG2 @staticmethod def js_vars(player: Player): return dict(my_symbol=player.participant.vars.get('symbol')) @staticmethod def live_method(player: Player, data: dict): fam = player.participant.vars.get('Ana_family_assignment') fam_state = _get_fam_state(player.session, fam, round_id='r3', board_len=15) board = fam_state['board'] member_ids = _get_fam_members(player.session, fam, player.subsession) if not data: broadcast = { 'ready': True, 'spotdiffR3': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False, } return {pid: broadcast for pid in member_ids} if 'move' in data and player.participant.vars.get('Ana_generation') == 2: move = data['move'] fam_state = _get_fam_state(player.session, fam, round_id='r3', board_len=15) board = fam_state['board'] clicked_count = sum(1 for x in board if x == 1) if board[move] == -1: if clicked_count < 5: board[move] = 1 else: board[move] = -1 player.session.vars['spotdiff_state'] = player.session.vars['spotdiff_state'] broadcast = { 'spotdiffR3': board, 'clicked_count': sum(1 for x in board if x == 1), 'game_end': False } return {pid: broadcast for pid in member_ids} @staticmethod def before_next_page(player: Player, timeout_happened=False): fam = player.participant.vars.get('Ana_family_assignment') if not fam or player.participant.vars.get('r3_score_saved'): return fam_state = _get_fam_state(player.session, fam, round_id='r3', board_len=15) board = fam_state['board'] CORRECT_SPOTS_R3 = [0, 2, 10, 12, 4] score = sum(1 for position in CORRECT_SPOTS_R3 if board[position] == 1) member_ids = _get_fam_members(player.session, fam, player.subsession) id_set = set(member_ids) for p in player.subsession.get_players(): if p.id_in_subsession in id_set: p.joint_task_r3_score = score p.participant.vars['r3_score_saved'] = True p.participant.vars['joint_task_r1to3_score'] = ( p.joint_task_r1_score + p.joint_task_r2_score + p.joint_task_r3_score) class Gen1RedirectToQuestionnaire(Page): @staticmethod def is_displayed(player): return (player.round_number == C.NUM_ROUNDS and player.participant.vars.get('Ana_generation') == 1 and player.participant.vars.get('state') == "active") @staticmethod def before_next_page(player: Player, timeout_happened=False): player.participant.vars['skip_to_questionnaire'] = True @staticmethod def app_after_this_page(player, upcoming_apps): if 'I_Post_Questionnaire_D' in upcoming_apps: return 'I_Post_Questionnaire_D' return None class DropJointTask(Page): @staticmethod def is_displayed(player): state = player.participant.vars.get('state') if state == "end": if player.participant.vars.get('Ana_generation') == 1 and player.round_number == C.NUM_ROUNDS: return True if player.participant.vars.get('Ana_generation') == 2 and player.round_number == C.NUM_ROUNDS: return True return False @staticmethod def vars_for_template(player): return { 'Ana_generation': player.participant.vars.get('Ana_generation'), 'auto_dropped': player.participant.vars.get('state') == "end", } page_sequence = [ WaitForSelection_G12, Introduction2, WaitForIntroduction_G12, Selection_G12, JointTask_Intro_G12, WaitForStart1, JointTask_R1p1, JointTask_R1p2, WaitForStart2, JointTask_R2p1, JointTask_R2p2, WaitForStart3, JointTask_R3p1, JointTask_R3p2, Gen1RedirectToQuestionnaire, DropJointTask, ]