import abc import time from typing import List from dice_roll import Subsession, Player, C from treatments import MatchingType class BaseMatching(metaclass=abc.ABCMeta): group_size = 2 @abc.abstractmethod def try_match(self, subsession: Subsession, waiting_players: List[Player]) -> List[Player]: raise NotImplementedError @staticmethod def waiting_too_long(p: Player): return time.time() - p.participant.matching_arrival_time > C.MAX_TIME_ON_WAITPAGE @staticmethod def set_matching_dropout(p: Player): p.set_dropout(is_responsible=False, err_msg="you could not be matched in time") class PartnerMatching(BaseMatching): def try_match(self, subsession: Subsession, waiting_players: List[Player]) -> List[Player]: """Receives a list of waiting players and tries to successfully match some to a group""" def is_valid_group(p1: Player, p2: Player) -> bool: """1) must not be self, 2) must not have been partner in round before, 3) must have corresponding roles A and B""" pt1, pt2 = p1.participant, p2.participant return p1 != p2 and pt1.partner != pt2.id_in_session and (round_number == 1 or (pt1.role != pt2.role)) def get_new_valid_partner(p1: Player) -> Player: """Yields a Player that is a potential partner for Player p1""" for p2 in waiting_players: if is_valid_group(p1, p2): yield p2 yield None round_number = subsession.round_number print("waiting_player:", waiting_players) for player in waiting_players: # drop if player has been waiting too long if self.waiting_too_long(player): self.set_matching_dropout(player) return [player] # try to match if partner := next(get_new_valid_partner(player)): return self.match(player, partner, first_round=round_number == 1) @staticmethod def match(player_a: Player, player_b: Player, first_round: bool) -> List[Player]: """Receives the two players that are matched to a group, sets their respective roles and partner ids and returns them""" participant_a, participant_b = player_a.participant, player_b.participant # save partner ids to later indentify if they were partners in this round participant_a.partner, participant_b.partner = ( participant_b.id_in_session, participant_a.id_in_session, ) if first_round: # set roles participant_a.role, participant_b.role = C.PLAYER_A, C.PLAYER_B return [player_a, player_b] class StrangerMatching(BaseMatching): batch_size = 10 # try_match: # first round: wait for 10 players, then everyone receives batch id (1...), receives player id within batch (1-10) # other rounds: new_partner = old_partner.id_in_batch + 1 def try_match(self, subsession: Subsession, waiting_players: List[Player]) -> List[Player]: """Receives a list of waiting players and tries to successfully match some to a group""" def next_partner_id_in_batch(pt) -> int: if pt.role == C.PLAYER_A: return (pt.partner + self.group_size) % self.batch_size else: return (pt.partner - self.group_size) % self.batch_size def is_valid_group(p1: Player, p2: Player) -> bool: pt1, pt2 = p1.participant, p2.participant if first_round: # must not be self return p1 != p2 else: # must be in same batch and next partner from batch return pt1.batch == pt2.batch and next_partner_id_in_batch(pt1) == pt2.id_in_batch def get_new_valid_partner(p1: Player) -> Player | None: """Yields a Player that is a potential partner for Player p1""" for p2 in waiting_players: if is_valid_group(p1, p2): return p2 def drop_waiting_players(): for p in waiting_players: if self.waiting_too_long(p): self.set_matching_dropout(p) return [p] round_number = subsession.round_number first_round = round_number == 1 # drop players that have been waiting too long drop_waiting_players() # in first round, require (batch_size) waiting players to start matching if first_round: session = subsession.session if len(waiting_players) < self.batch_size: if not session.open_matching: return else: session.open_matching = True for player in waiting_players: if partner := get_new_valid_partner(player): return self.match(subsession, player, partner, first_round) def match( self, subsession: Subsession, player_a: Player, player_b: Player, first_round: bool, ) -> List[Player]: """Receives the two players that are matched to a group, sets their respective roles, batch_nr, id_in_batch and partner ids and returns them""" session = subsession.session participant_a, participant_b = player_a.participant, player_b.participant if first_round: # set roles participant_a.role, participant_b.role = C.PLAYER_A, C.PLAYER_B # set batch id batch_nr = session.batch_nr participant_a.batch, participant_b.batch = batch_nr, batch_nr # set id in batch next_id_in_batch = session.nr_players_in_batch % self.batch_size participant_a.id_in_batch, participant_b.id_in_batch = ( next_id_in_batch, next_id_in_batch + 1, ) session.nr_players_in_batch += self.group_size if self.batch_size <= session.nr_players_in_batch: session.batch_nr += 1 session.open_matching = False session.nr_players_in_batch = 0 # set partner ids in batch participant_a.partner, participant_b.partner = ( participant_b.id_in_batch, participant_a.id_in_batch, ) return [player_a, player_b] def get_matching(matching_type: MatchingType): if matching_type == MatchingType.PARTNER: return PartnerMatching() else: return StrangerMatching()