Skip to content

Commit

Permalink
Use different search expansion for top players (#847)
Browse files Browse the repository at this point in the history
* Use different search expansion for top players

* Fix typo
  • Loading branch information
BlackYps authored Oct 23, 2021
1 parent 19bf582 commit ad70490
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 20 deletions.
5 changes: 4 additions & 1 deletion server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ def __init__(self):
self.NEWBIE_MIN_GAMES = 10
self.START_RATING_MEAN = 1500
self.START_RATING_DEV = 500
self.TOP_PLAYER_MIN_RATING = 1600
self.HIGH_RATED_PLAYER_MIN_RATING = 1400
self.TOP_PLAYER_MIN_RATING = 2000

# Values for the custom (i.e. not trueskill) game quality metric used by the matchmaker
self.MINIMUM_GAME_QUALITY = 0.4
Expand Down Expand Up @@ -111,6 +112,8 @@ def __init__(self):
self.LADDER_ANTI_REPETITION_LIMIT = 2
self.LADDER_SEARCH_EXPANSION_MAX = 0.25
self.LADDER_SEARCH_EXPANSION_STEP = 0.05
self.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX = 0.3
self.LADDER_TOP_PLAYER_SEARCH_EXPANSION_STEP = 0.15
# The maximum amount of time in seconds) to wait between pops.
self.QUEUE_POP_TIME_MAX = 180
# The number of possible matches we would like to have when the queue
Expand Down
2 changes: 1 addition & 1 deletion server/matchmaker/algorithm/random_newbies.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def find(
unmatched_newbies: List[Search] = []
first_opponent = None
for search in searches:
if search.has_top_player():
if search.has_high_rated_player():
continue
elif search.has_newbie():
unmatched_newbies.append(search)
Expand Down
2 changes: 1 addition & 1 deletion server/matchmaker/algorithm/team_matchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def assign_game_quality(self, match: Match, team_size: int) -> GameCandidate:
# Visually this creates a cone in the unfairness-rating_variety plane
# that slowly raises with the time bonuses.
quality = 1 - sqrt(unfairness ** 2 + rating_variety ** 2) + time_bonus
if not any(team.has_top_player() for team in match):
if not any(team.has_high_rated_player() for team in match):
quality += newbie_bonus
self._logger.debug(
"bonuses: %s rating disparity: %s -> unfairness: %f deviation: %f -> variety: %f -> game quality: %f",
Expand Down
41 changes: 32 additions & 9 deletions server/matchmaker/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


def get_average_rating(searches):
return statistics.mean(mean - 3 * dev for s in searches for mean, dev in s.raw_ratings)
return statistics.mean(itertools.chain(*[s.displayed_ratings for s in searches]))


@with_logger
Expand Down Expand Up @@ -74,8 +74,12 @@ def has_newbie(self) -> bool:
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(map(lambda rating_tuple: rating_tuple[0], self.ratings))
max_rating = max(self.displayed_ratings)
return max_rating >= config.TOP_PLAYER_MIN_RATING

@property
Expand All @@ -90,16 +94,24 @@ def ratings(self):

@property
def cumulative_rating(self):
return sum(mean - 3 * dev for mean, dev in self.raw_ratings)
return sum(self.displayed_ratings)

@property
def average_rating(self):
return statistics.mean(mean - 3 * dev for mean, dev in self.raw_ratings)
return statistics.mean(self.displayed_ratings)

@property
def raw_ratings(self):
return [player.ratings[self.rating_type] for player in self.players]

@property
def displayed_ratings(self):
"""
The client always displays mean - 3 * dev as a player's rating.
So generally this is perceived as a player's true rating.
"""
return [mean - 3 * dev for mean, dev in self.raw_ratings]

def _nearby_rating_range(self, delta):
"""
Returns 'boundary' mu values for player matching. Adjust delta for
Expand Down Expand Up @@ -131,12 +143,19 @@ def search_expansion(self) -> float:
The threshold will expand linearly with every failed matching attempt
until it reaches the specified MAX.
"""
return min(
self._failed_matching_attempts * config.LADDER_SEARCH_EXPANSION_STEP,
config.LADDER_SEARCH_EXPANSION_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
)
else:
return min(
self._failed_matching_attempts * config.LADDER_SEARCH_EXPANSION_STEP,
config.LADDER_SEARCH_EXPANSION_MAX
)

def register_failed_matching_attempt(self):
"""
Expand Down Expand Up @@ -293,6 +312,10 @@ def average_rating(self):
def raw_ratings(self):
return list(itertools.chain(*[s.raw_ratings for s in self.searches]))

@property
def displayed_ratings(self):
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)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/test_matchmaker_algorithm_stable_marriage.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def test_odd_number_of_unmatched_newbies(player_factory):
newbie1 = Search([player_factory(-250, 500, ladder_games=9)])
newbie2 = Search([player_factory(750, 500, ladder_games=9)])
newbie3 = Search([player_factory(1500, 500, ladder_games=9)])
pro = Search([player_factory(1500, 10, ladder_games=100)])
pro = Search([player_factory(1500, 70, ladder_games=100)])
pro.register_failed_matching_attempt()

searches = [newbie1, pro, newbie2, newbie3]
Expand All @@ -457,7 +457,7 @@ def test_matchmaker(player_factory):
pro_that_matches1.register_failed_matching_attempt()
pro_that_matches2 = Search([player_factory(1750, 50, ladder_games=100)])
pro_that_matches2.register_failed_matching_attempt()
pro_alone = Search([player_factory(1550, 50, ladder_games=100)])
pro_alone = Search([player_factory(1550, 70, ladder_games=100)])
pro_alone.register_failed_matching_attempt()

top_player = Search([player_factory(2100, 50, ladder_games=200)])
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/test_matchmaker_algorithm_team_matchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def test_game_quality_time_bonus(s):
team_b.register_failed_matching_attempt()
quality_after = matchmaker.assign_game_quality((team_a, team_b), 3).quality

if team_a.has_top_player() or team_b.has_top_player():
if team_a.has_high_rated_player() or team_b.has_high_rated_player():
num_newbies = 0
else:
num_newbies = team_a.num_newbies() + team_b.num_newbies()
Expand All @@ -286,7 +286,7 @@ def test_game_quality_max_time_bonus(s):
team_b.register_failed_matching_attempt()
quality_after = matchmaker.assign_game_quality((team_a, team_b), 3).quality

if team_a.has_top_player() or team_b.has_top_player():
if team_a.has_high_rated_player() or team_b.has_high_rated_player():
num_newbies = 0
else:
num_newbies = team_a.num_newbies() + team_b.num_newbies()
Expand Down
27 changes: 23 additions & 4 deletions tests/unit_tests/test_matchmaker_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from hypothesis import given
from hypothesis import strategies as st

import server.config as config
from server.config import config
from server.matchmaker import CombinedSearch, MapPool, PopTimer, Search
from server.players import PlayerState
from server.rating import RatingType
Expand Down Expand Up @@ -174,14 +174,14 @@ def test_search_boundaries(matchmaker_players):
assert p1.ratings[RatingType.LADDER_1V1][0] < s1.boundary_75[1]


def test_search_expansion_controlled_by_failed_matching_attempts(matchmaker_players, mocker):
p1 = matchmaker_players[0]
def test_search_expansion_controlled_by_failed_matching_attempts(matchmaker_players):
p1 = matchmaker_players[1]
s1 = Search([p1])

assert s1.search_expansion == 0.0

s1.register_failed_matching_attempt()
assert s1.search_expansion > 0.0
assert s1.search_expansion == config.LADDER_SEARCH_EXPANSION_STEP

# Make sure that the expansion stops at some point
for _ in range(100):
Expand All @@ -193,6 +193,25 @@ def test_search_expansion_controlled_by_failed_matching_attempts(matchmaker_play
assert e1 == config.LADDER_SEARCH_EXPANSION_MAX


def test_search_expansion_for_top_players(matchmaker_players):
p1 = matchmaker_players[0]
s1 = Search([p1])

assert s1.search_expansion == 0.0

s1.register_failed_matching_attempt()
assert s1.search_expansion == config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_STEP

# Make sure that the expansion stops at some point
for _ in range(100):
s1.register_failed_matching_attempt()
e1 = s1.search_expansion

s1.register_failed_matching_attempt()
assert e1 == s1.search_expansion
assert e1 == config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX


@pytest.mark.asyncio
async def test_search_await(matchmaker_players):
p1, p2, _, _, _, _ = matchmaker_players
Expand Down

0 comments on commit ad70490

Please sign in to comment.