Module server.matchmaker.search
Functions
def get_average_rating(searches)
-
Expand source code
def get_average_rating(searches): return statistics.mean(itertools.chain(*[s.displayed_ratings for s in searches]))
Classes
class CombinedSearch (*searches: Search)
-
Expand source code
class CombinedSearch(Search): def __init__(self, *searches: Search): assert searches rating_type = searches[0].rating_type assert all(map(lambda s: s.rating_type == rating_type, searches)) self.rating_type = rating_type self.searches = searches @property def players(self) -> list[Player]: return list(itertools.chain(*[s.players for s in self.searches])) @property def ratings(self) -> list[Rating]: return list(itertools.chain(*[s.ratings for s in self.searches])) @property def cumulative_rating(self) -> float: return sum(s.cumulative_rating for s in self.searches) @property def average_rating(self) -> float: return get_average_rating(self.searches) @property def raw_ratings(self) -> list[Rating]: return list(itertools.chain(*[s.raw_ratings for s in self.searches])) @property def displayed_ratings(self) -> list[float]: return list(itertools.chain(*[s.displayed_ratings for s in self.searches])) @property def failed_matching_attempts(self) -> int: return max(search.failed_matching_attempts for search in self.searches) def register_failed_matching_attempt(self): for search in self.searches: search.register_failed_matching_attempt() @property def match_threshold(self) -> float: """ Defines the threshold for game quality """ return min(s.match_threshold for s in self.searches) @property def is_matched(self) -> bool: return all(s.is_matched for s in self.searches) def done(self) -> bool: return all(s.done() for s in self.searches) @property def is_cancelled(self) -> bool: return any(s.is_cancelled for s in self.searches) def match(self, other: "Search"): """ Mark as matched with given opponent """ self._logger.info("Combined search matched %s with %s", self.players, other.players) for s in self.searches: s.match(other) async def await_match(self): """ Wait for this search to complete """ await asyncio.wait({s.await_match() for s in self.searches}) def cancel(self): """ Cancel searching for a match """ for s in self.searches: s.cancel() def __str__(self): return f"CombinedSearch({', '.join(str(s) for s in self.searches)})" def __repr__(self): return f"CombinedSearch({', '.join(str(s) for s in self.searches)})" def get_original_searches(self) -> list[Search]: """ Returns the searches of which this CombinedSearch is comprised """ return list(self.searches)
Represents the state of a users search for a match.
Ancestors
Instance variables
prop average_rating : float
-
Expand source code
@property def average_rating(self) -> float: return get_average_rating(self.searches)
prop cumulative_rating : float
-
Expand source code
@property def cumulative_rating(self) -> float: return sum(s.cumulative_rating for s in self.searches)
prop failed_matching_attempts : int
-
Expand source code
@property def failed_matching_attempts(self) -> int: return max(search.failed_matching_attempts for search in self.searches)
prop is_cancelled : bool
-
Expand source code
@property def is_cancelled(self) -> bool: return any(s.is_cancelled for s in self.searches)
prop is_matched : bool
-
Expand source code
@property def is_matched(self) -> bool: return all(s.is_matched for s in self.searches)
prop match_threshold : float
-
Expand source code
@property def match_threshold(self) -> float: """ Defines the threshold for game quality """ return min(s.match_threshold for s in self.searches)
Defines the threshold for game quality
prop players : list[Player]
-
Expand source code
@property def players(self) -> list[Player]: return list(itertools.chain(*[s.players for s in self.searches]))
prop ratings : list[Rating]
-
Expand source code
@property def ratings(self) -> list[Rating]: return list(itertools.chain(*[s.ratings for s in self.searches]))
prop raw_ratings : list[Rating]
-
Expand source code
@property def raw_ratings(self) -> list[Rating]: return list(itertools.chain(*[s.raw_ratings for s in self.searches]))
Methods
def done(self) ‑> bool
-
Expand source code
def done(self) -> bool: return all(s.done() for s in self.searches)
def get_original_searches(self) ‑> list[Search]
-
Expand source code
def get_original_searches(self) -> list[Search]: """ Returns the searches of which this CombinedSearch is comprised """ return list(self.searches)
Returns the searches of which this CombinedSearch is comprised
Inherited members
class Search (players: list[Player],
start_time: float | None = None,
rating_type: str = 'ladder_1v1',
on_matched: Callable[[ForwardRef('Search'), ForwardRef('Search')], Any] = <function Search.<lambda>>)-
Expand source code
@with_logger class Search: """ Represents the state of a users search for a match. """ def __init__( self, players: list[Player], start_time: Optional[float] = None, rating_type: str = RatingType.LADDER_1V1, on_matched: OnMatchedCallback = lambda _1, _2: None ): assert isinstance(players, list) for player in players: assert player.ratings[rating_type] is not None self.players = players self.rating_type = rating_type self.start_time = start_time or time.time() self._match = asyncio.get_event_loop().create_future() self._failed_matching_attempts = 0 self.on_matched = on_matched # Precompute this self.quality_against_self = self.quality_with(self) def adjusted_rating(self, player: Player) -> Rating: """ Returns an adjusted mean with a simple linear interpolation between current mean and a specified base mean """ mean, dev = player.ratings[self.rating_type] game_count = player.game_count[self.rating_type] adjusted_mean = ((config.NEWBIE_MIN_GAMES - game_count) * config.NEWBIE_BASE_MEAN + game_count * mean) / config.NEWBIE_MIN_GAMES return Rating(adjusted_mean, dev) def is_newbie(self, player: Player) -> bool: return player.game_count[self.rating_type] <= config.NEWBIE_MIN_GAMES def is_single_party(self) -> bool: return len(self.players) == 1 def has_newbie(self) -> bool: for player in self.players: if self.is_newbie(player): return True return False def num_newbies(self) -> int: return sum(self.is_newbie(player) for player in self.players) def has_high_rated_player(self) -> bool: max_rating = max(self.displayed_ratings) return max_rating >= config.HIGH_RATED_PLAYER_MIN_RATING def has_top_player(self) -> bool: max_rating = max(self.displayed_ratings) return max_rating >= config.TOP_PLAYER_MIN_RATING @property def ratings(self) -> list[Rating]: ratings = [] for player, rating in zip(self.players, self.raw_ratings): # New players (less than config.NEWBIE_MIN_GAMES games) match against less skilled opponents if self.is_newbie(player): rating = self.adjusted_rating(player) ratings.append(rating) return ratings @property def cumulative_rating(self) -> float: return sum(self.displayed_ratings) @property def average_rating(self) -> float: return statistics.mean(self.displayed_ratings) @property def raw_ratings(self) -> list[Rating]: return [player.ratings[self.rating_type] for player in self.players] @property def displayed_ratings(self) -> list[float]: """ The client always displays mean - 3 * dev as a player's rating. So generally this is perceived as a player's true rating. """ return [rating.displayed() for rating in self.raw_ratings] def _nearby_rating_range(self, delta: int) -> tuple[int, int]: """ Returns 'boundary' mu values for player matching. Adjust delta for different game qualities. """ mu, _ = self.ratings[0] # Takes the rating of the first player, only works for 1v1 rounded_mu = int(math.ceil(mu / 10) * 10) # Round to 10 return rounded_mu - delta, rounded_mu + delta @property def boundary_80(self) -> tuple[int, int]: """ Achieves roughly 80% quality. """ return self._nearby_rating_range(200) @property def boundary_75(self) -> tuple[int, int]: """ Achieves roughly 75% quality. FIXME - why is it MORE restrictive??? """ return self._nearby_rating_range(100) @property def failed_matching_attempts(self) -> int: return self._failed_matching_attempts @property def search_expansion(self) -> float: """ Defines how much to expand the search range of game quality due to waiting time. The threshold will expand linearly with every failed matching attempt until it reaches the specified MAX. Top players use bigger values to make matching easier. """ if self.has_top_player(): return min( self._failed_matching_attempts * config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_STEP, config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX ) elif self.has_newbie(): return min( self._failed_matching_attempts * config.LADDER_NEWBIE_SEARCH_EXPANSION_STEP, config.LADDER_NEWBIE_SEARCH_EXPANSION_MAX ) else: return min( self._failed_matching_attempts * config.LADDER_SEARCH_EXPANSION_STEP, config.LADDER_SEARCH_EXPANSION_MAX ) def register_failed_matching_attempt(self): """ Signal that matchmaker tried to match this search but was unsuccessful and increase internal counter by one. """ self._failed_matching_attempts += 1 @property def match_threshold(self) -> float: """ Defines the threshold for game quality The base minimum quality is determined as 80% of the quality of a game against a copy of yourself. This is decreased by `self.search_expansion` if search is to be expanded. """ return max(0.8 * self.quality_against_self - self.search_expansion, 0) def quality_with(self, other: "Search") -> float: assert all(other.raw_ratings) assert other.players team1 = [trueskill.Rating(*rating) for rating in self.ratings] team2 = [trueskill.Rating(*rating) for rating in other.ratings] return trueskill.quality([team1, team2]) @property def is_matched(self) -> bool: return self._match.done() and not self._match.cancelled() def done(self) -> bool: return self._match.done() @property def is_cancelled(self) -> bool: return self._match.cancelled() def matches_with(self, other: "Search") -> bool: """ Determine if this search is compatible with other given search according to both wishes. """ if not isinstance(other, Search): return False quality = self.quality_with(other) return self._match_quality_acceptable(other, quality) def _match_quality_acceptable(self, other: "Search", quality: float) -> bool: """ Determine if the given match quality is acceptable. This gets it's own function so we can call it from the Matchmaker using a cached `quality` value. """ # NOTE: We are assuming for optimization purposes that quality is # symmetric. If this ever changes, update here return (quality >= self.match_threshold and quality >= other.match_threshold) def match(self, other: "Search"): """ Mark as matched with given opponent """ self._logger.info("Matched %s with %s", self, other) self.on_matched(self, other) for player, raw_rating in zip(self.players, self.raw_ratings): if self.is_newbie(player): mean, dev = raw_rating adjusted_mean, adjusted_dev = self.adjusted_rating(player) self._logger.info( "Adjusted mean rating for %s with %d games from %.1f to %.1f", player.login, player.game_count[self.rating_type], mean, adjusted_mean ) self._match.set_result(other) async def await_match(self): """ Wait for this search to complete """ await asyncio.wait_for(self._match, None) return self._match def cancel(self): """ Cancel searching for a match """ self._match.cancel() def __str__(self) -> str: return ( f"Search({self.rating_type}, {self._players_repr()}, threshold=" f"{self.match_threshold:.2f}, expansion={self.search_expansion:.2f})" ) def _players_repr(self) -> str: contents = ", ".join( f"Player({p.login}, {p.id}, {p.ratings[self.rating_type]})" for p in self.players ) return f"[{contents}]" def __repr__(self) -> str: """For debugging""" return ( f"Search({[p.login for p in self.players]}, {self.average_rating}" f"{f', FMA: {self.failed_matching_attempts}' if self.failed_matching_attempts else ''}" f"{', has_newbie)' if self.has_newbie() else ')'}" ) def get_original_searches(self) -> list["Search"]: """ Returns the searches of which this Search is comprised, as if it were a CombinedSearch of one """ return [self]
Represents the state of a users search for a match.
Subclasses
Instance variables
prop average_rating : float
-
Expand source code
@property def average_rating(self) -> float: return statistics.mean(self.displayed_ratings)
prop boundary_75 : tuple[int, int]
-
Expand source code
@property def boundary_75(self) -> tuple[int, int]: """ Achieves roughly 75% quality. FIXME - why is it MORE restrictive??? """ return self._nearby_rating_range(100)
Achieves roughly 75% quality. FIXME - why is it MORE restrictive???
prop boundary_80 : tuple[int, int]
-
Expand source code
@property def boundary_80(self) -> tuple[int, int]: """ Achieves roughly 80% quality. """ return self._nearby_rating_range(200)
Achieves roughly 80% quality.
prop cumulative_rating : float
-
Expand source code
@property def cumulative_rating(self) -> float: return sum(self.displayed_ratings)
prop displayed_ratings : list[float]
-
Expand source code
@property def displayed_ratings(self) -> list[float]: """ The client always displays mean - 3 * dev as a player's rating. So generally this is perceived as a player's true rating. """ return [rating.displayed() for rating in self.raw_ratings]
The client always displays mean - 3 * dev as a player's rating. So generally this is perceived as a player's true rating.
prop failed_matching_attempts : int
-
Expand source code
@property def failed_matching_attempts(self) -> int: return self._failed_matching_attempts
prop is_cancelled : bool
-
Expand source code
@property def is_cancelled(self) -> bool: return self._match.cancelled()
prop is_matched : bool
-
Expand source code
@property def is_matched(self) -> bool: return self._match.done() and not self._match.cancelled()
prop match_threshold : float
-
Expand source code
@property def match_threshold(self) -> float: """ Defines the threshold for game quality The base minimum quality is determined as 80% of the quality of a game against a copy of yourself. This is decreased by `self.search_expansion` if search is to be expanded. """ return max(0.8 * self.quality_against_self - self.search_expansion, 0)
Defines the threshold for game quality
The base minimum quality is determined as 80% of the quality of a game against a copy of yourself. This is decreased by
self.search_expansion
if search is to be expanded. prop ratings : list[Rating]
-
Expand source code
@property def ratings(self) -> list[Rating]: ratings = [] for player, rating in zip(self.players, self.raw_ratings): # New players (less than config.NEWBIE_MIN_GAMES games) match against less skilled opponents if self.is_newbie(player): rating = self.adjusted_rating(player) ratings.append(rating) return ratings
prop raw_ratings : list[Rating]
-
Expand source code
@property def raw_ratings(self) -> list[Rating]: return [player.ratings[self.rating_type] for player in self.players]
prop search_expansion : float
-
Expand source code
@property def search_expansion(self) -> float: """ Defines how much to expand the search range of game quality due to waiting time. The threshold will expand linearly with every failed matching attempt until it reaches the specified MAX. Top players use bigger values to make matching easier. """ if self.has_top_player(): return min( self._failed_matching_attempts * config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_STEP, config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX ) elif self.has_newbie(): return min( self._failed_matching_attempts * config.LADDER_NEWBIE_SEARCH_EXPANSION_STEP, config.LADDER_NEWBIE_SEARCH_EXPANSION_MAX ) else: return min( self._failed_matching_attempts * config.LADDER_SEARCH_EXPANSION_STEP, config.LADDER_SEARCH_EXPANSION_MAX )
Defines how much to expand the search range of game quality due to waiting time.
The threshold will expand linearly with every failed matching attempt until it reaches the specified MAX.
Top players use bigger values to make matching easier.
Methods
def adjusted_rating(self,
player: Player) ‑> Rating-
Expand source code
def adjusted_rating(self, player: Player) -> Rating: """ Returns an adjusted mean with a simple linear interpolation between current mean and a specified base mean """ mean, dev = player.ratings[self.rating_type] game_count = player.game_count[self.rating_type] adjusted_mean = ((config.NEWBIE_MIN_GAMES - game_count) * config.NEWBIE_BASE_MEAN + game_count * mean) / config.NEWBIE_MIN_GAMES return Rating(adjusted_mean, dev)
Returns an adjusted mean with a simple linear interpolation between current mean and a specified base mean
async def await_match(self)
-
Expand source code
async def await_match(self): """ Wait for this search to complete """ await asyncio.wait_for(self._match, None) return self._match
Wait for this search to complete
def cancel(self)
-
Expand source code
def cancel(self): """ Cancel searching for a match """ self._match.cancel()
Cancel searching for a match
def done(self) ‑> bool
-
Expand source code
def done(self) -> bool: return self._match.done()
def get_original_searches(self) ‑> list['Search']
-
Expand source code
def get_original_searches(self) -> list["Search"]: """ Returns the searches of which this Search is comprised, as if it were a CombinedSearch of one """ return [self]
Returns the searches of which this Search is comprised, as if it were a CombinedSearch of one
def has_high_rated_player(self) ‑> bool
-
Expand source code
def has_high_rated_player(self) -> bool: max_rating = max(self.displayed_ratings) return max_rating >= config.HIGH_RATED_PLAYER_MIN_RATING
def has_newbie(self) ‑> bool
-
Expand source code
def has_newbie(self) -> bool: for player in self.players: if self.is_newbie(player): return True return False
def has_top_player(self) ‑> bool
-
Expand source code
def has_top_player(self) -> bool: max_rating = max(self.displayed_ratings) return max_rating >= config.TOP_PLAYER_MIN_RATING
def is_newbie(self,
player: Player) ‑> bool-
Expand source code
def is_newbie(self, player: Player) -> bool: return player.game_count[self.rating_type] <= config.NEWBIE_MIN_GAMES
def is_single_party(self) ‑> bool
-
Expand source code
def is_single_party(self) -> bool: return len(self.players) == 1
def match(self,
other: Search)-
Expand source code
def match(self, other: "Search"): """ Mark as matched with given opponent """ self._logger.info("Matched %s with %s", self, other) self.on_matched(self, other) for player, raw_rating in zip(self.players, self.raw_ratings): if self.is_newbie(player): mean, dev = raw_rating adjusted_mean, adjusted_dev = self.adjusted_rating(player) self._logger.info( "Adjusted mean rating for %s with %d games from %.1f to %.1f", player.login, player.game_count[self.rating_type], mean, adjusted_mean ) self._match.set_result(other)
Mark as matched with given opponent
def matches_with(self,
other: Search) ‑> bool-
Expand source code
def matches_with(self, other: "Search") -> bool: """ Determine if this search is compatible with other given search according to both wishes. """ if not isinstance(other, Search): return False quality = self.quality_with(other) return self._match_quality_acceptable(other, quality)
Determine if this search is compatible with other given search according to both wishes.
def num_newbies(self) ‑> int
-
Expand source code
def num_newbies(self) -> int: return sum(self.is_newbie(player) for player in self.players)
def quality_with(self,
other: Search) ‑> float-
Expand source code
def quality_with(self, other: "Search") -> float: assert all(other.raw_ratings) assert other.players team1 = [trueskill.Rating(*rating) for rating in self.ratings] team2 = [trueskill.Rating(*rating) for rating in other.ratings] return trueskill.quality([team1, team2])
def register_failed_matching_attempt(self)
-
Expand source code
def register_failed_matching_attempt(self): """ Signal that matchmaker tried to match this search but was unsuccessful and increase internal counter by one. """ self._failed_matching_attempts += 1
Signal that matchmaker tried to match this search but was unsuccessful and increase internal counter by one.