Skip to content

Commit

Permalink
Feature/#713 Generalize newbie rating adjustments (#714)
Browse files Browse the repository at this point in the history
* Include newbie adjustments for all rating types and team sizes
  • Loading branch information
Askaholic authored Feb 6, 2021
1 parent 6f4d4a5 commit 08d1d8f
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 68 deletions.
27 changes: 12 additions & 15 deletions server/matchmaker/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,28 +110,25 @@ class RandomlyMatchNewbies(MatchmakingPolicy):
def find(self, searches: Iterable[Search]) -> Dict[Search, Search]:
self.matches.clear()

unmatched_newbies = [
search for search in searches
if search.is_single_ladder_newbie()
]
unmatched_newbies = []
first_opponent = None
for search in searches:
if search.has_top_player():
continue

if search.has_newbie():
unmatched_newbies.append(search)
elif not first_opponent and search.failed_matching_attempts >= 1:
first_opponent = search

while len(unmatched_newbies) >= 2:
newbie1 = unmatched_newbies.pop()
newbie2 = unmatched_newbies.pop()
self._match(newbie1, newbie2)

if len(unmatched_newbies) == 1:
if unmatched_newbies and first_opponent:
newbie = unmatched_newbies[0]

default_if_no_available_opponent = None

opponent = next((
search for search in searches
if search != newbie and search not in self.matches
and search.is_single_party() and search.has_no_top_player()
), default_if_no_available_opponent)
if opponent is not default_if_no_available_opponent:
self._match(newbie, opponent)
self._match(newbie, first_opponent)

return self.matches

Expand Down
38 changes: 17 additions & 21 deletions server/matchmaker/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,43 +51,39 @@ def __init__(
# Precompute this
self.quality_against_self = self.quality_with(self)

@staticmethod
def adjusted_rating(player: Player):
def adjusted_rating(self, player: Player):
"""
Returns an adjusted mean with a simple linear interpolation between current mean and a specified base mean
"""
mean, dev = player.ratings[RatingType.LADDER_1V1]
adjusted_mean = ((config.NEWBIE_MIN_GAMES - player.game_count[RatingType.LADDER_1V1]) * config.NEWBIE_BASE_MEAN
+ player.game_count[RatingType.LADDER_1V1] * mean) / config.NEWBIE_MIN_GAMES
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 adjusted_mean, dev

@staticmethod
def _is_ladder_newbie(player: Player) -> bool:
return player.game_count[RatingType.LADDER_1V1] <= config.NEWBIE_MIN_GAMES

def is_ladder1v1_search(self) -> bool:
return self.rating_type == RatingType.LADDER_1V1
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 is_single_ladder_newbie(self) -> bool:
return (
self.is_single_party()
and self._is_ladder_newbie(self.players[0])
and self.is_ladder1v1_search()
)
def has_newbie(self) -> bool:
for player in self.players:
if self.is_newbie(player):
return True

return False

def has_no_top_player(self) -> bool:
def has_top_player(self) -> bool:
max_rating = max(map(lambda rating_tuple: rating_tuple[0], self.ratings))
return max_rating < config.TOP_PLAYER_MIN_RATING
return max_rating >= config.TOP_PLAYER_MIN_RATING

@property
def ratings(self):
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_ladder1v1_search() and self._is_ladder_newbie(player):
if self.is_newbie(player):
rating = self.adjusted_rating(player)
ratings.append(rating)
return ratings
Expand Down Expand Up @@ -209,7 +205,7 @@ def match(self, other: "Search"):
self.on_matched(self, other)

for player, raw_rating in zip(self.players, self.raw_ratings):
if self.is_ladder1v1_search() and self._is_ladder_newbie(player):
if self.is_newbie(player):
mean, dev = raw_rating
adjusted_mean, adjusted_dev = self.adjusted_rating(player)
self._logger.info(
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/test_teammatchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async def test_info_message(lobby_server):
boundaries = queue["boundary_80s"]

if queue["queue_name"] == "tmm2v2":
assert boundaries == [[1300, 1700]]
assert boundaries == [[300, 700]]
else:
assert boundaries == []

Expand Down
59 changes: 33 additions & 26 deletions tests/unit_tests/test_matchmaker_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
from server.rating import RatingType


@st.composite
def st_rating(draw):
"""Strategy for generating rating tuples"""
return (
draw(st.floats(min_value=-100., max_value=2500.)),
draw(st.floats(min_value=0.001, max_value=500.))
)


@pytest.fixture(scope="session")
def player_factory(player_factory):
return functools.partial(
Expand Down Expand Up @@ -42,37 +51,32 @@ def matchmaker_players_all_match(player_factory):
player_factory("Rhiza", player_id=5, ladder_rating=(1500, 50))


def test_is_ladder_newbie(matchmaker_players):
pro, _, _, _, _, newbie = matchmaker_players
assert Search._is_ladder_newbie(pro) is False
assert Search._is_ladder_newbie(newbie)


def test_is_single_newbie(matchmaker_players):
pro, _, _, _, _, newbie = matchmaker_players

single_newbie = Search([newbie])
single_pro = Search([pro])
two_newbies = Search([newbie, newbie])
two_pros = Search([pro, pro])
two_mixed = Search([newbie, pro])
def test_newbie_detection(matchmaker_players):
pro, joe, _, _, _, newbie = matchmaker_players
pro_search = Search([pro])
newbie_search = Search([newbie])
newb_team_search = Search([joe, newbie])
pro_team_search = Search([pro, joe])

assert single_newbie.is_single_ladder_newbie()
assert single_pro.is_single_ladder_newbie() is False
assert two_newbies.is_single_ladder_newbie() is False
assert two_pros.is_single_ladder_newbie() is False
assert two_mixed.is_single_ladder_newbie() is False
assert pro_search.has_newbie() is False
assert pro_search.is_newbie(pro) is False
assert newbie_search.has_newbie() is True
assert newbie_search.is_newbie(newbie) is True
assert newb_team_search.has_newbie() is True
assert pro_team_search.has_newbie() is False


def test_newbies_have_adjusted_rating(matchmaker_players):
pro, _, _, _, _, newbie = matchmaker_players
s1, s6 = Search([pro]), Search([newbie])
assert s1.ratings[0] == pro.ratings[RatingType.LADDER_1V1]
assert s6.ratings[0] != newbie.ratings[RatingType.LADDER_1V1]
assert s6.ratings[0] < newbie.ratings[RatingType.LADDER_1V1]


def test_search_threshold(matchmaker_players):
s = Search([matchmaker_players[0]])
@given(rating=st_rating())
def test_search_threshold(player_factory, rating):
player = player_factory("Player", ladder_rating=rating)
s = Search([player])
assert s.match_threshold <= 1
assert s.match_threshold >= 0

Expand Down Expand Up @@ -103,10 +107,13 @@ def test_search_threshold_of_team_new_players_is_low(player_factory):
assert s.match_threshold <= 0.4


def test_search_quality_equivalence(matchmaker_players):
p1, _, _, p4, _, _ = matchmaker_players
s1, s4 = Search([p1]), Search([p4])
assert s1.quality_with(s4) == s4.quality_with(s1)
@given(rating1=st_rating(), rating2=st_rating())
def test_search_quality_equivalence(player_factory, rating1, rating2):
p1 = player_factory("Player1", ladder_rating=rating1)
p2 = player_factory("Player2", ladder_rating=rating2)
s1 = Search([p1])
s2 = Search([p2])
assert s1.quality_with(s2) == s2.quality_with(s1)


def test_search_quality(matchmaker_players):
Expand Down
101 changes: 96 additions & 5 deletions tests/unit_tests/test_matchmaker_queue_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ def test_random_newbie_matching_is_symmetric(player_factory):
searches = [s1, s2, s3, s4, s5, s6]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert matches

for search in matches:
opponent = matches[search]
assert matches[opponent] == search
Expand All @@ -310,6 +312,7 @@ def test_newbies_are_forcefully_matched_with_newbies(player_factory):
newbie1 = Search([player_factory(0, 500, ladder_games=9)])
newbie2 = Search([player_factory(1500, 500, ladder_games=9)])
pro = Search([player_factory(1500, 10, ladder_games=100)])
pro.register_failed_matching_attempt()

searches = [newbie1, pro, newbie2]
matches = algorithm.RandomlyMatchNewbies().find(searches)
Expand All @@ -318,31 +321,113 @@ def test_newbies_are_forcefully_matched_with_newbies(player_factory):
assert matches[newbie2] == newbie1


def test_newbie_team_matched_with_newbie_team(player_factory):
newbie1 = Search([
player_factory(0, 500, ladder_games=9),
player_factory(0, 500, ladder_games=9)
])
newbie2 = Search([
player_factory(1500, 500, ladder_games=9),
player_factory(1500, 500, ladder_games=9)
])

searches = [newbie1, newbie2]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert matches[newbie1] == newbie2
assert matches[newbie2] == newbie1


def test_partial_newbie_team_matched_with_newbie_team(player_factory):
partial_newbie = Search([
player_factory(0, 500, ladder_games=9),
player_factory(0, 500, ladder_games=100)
])
newbie = Search([
player_factory(1500, 500, ladder_games=9),
player_factory(1500, 500, ladder_games=9)
])

searches = [partial_newbie, newbie]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert matches[partial_newbie] == newbie
assert matches[newbie] == partial_newbie


def test_newbie_and_top_rated_team_not_matched_randomly(player_factory):
newbie_and_top_rated = Search([
player_factory(0, 500, ladder_games=9),
player_factory(2500, 10, ladder_games=1000)
])
newbie = Search([
player_factory(1500, 500, ladder_games=9),
player_factory(1500, 500, ladder_games=9)
])

searches = [newbie_and_top_rated, newbie]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert not matches


def test_unmatched_newbies_forcefully_match_pros(player_factory):
newbie = Search([player_factory(1500, 500, ladder_games=0)])
pro = Search([player_factory(1400, 10, ladder_games=100)])

searches = [newbie, pro]
matches = algorithm.RandomlyMatchNewbies().find(searches)
# No match if the pro is on their first attempt
assert len(matches) == 0

pro.register_failed_matching_attempt()
matches = algorithm.RandomlyMatchNewbies().find(searches)
assert len(matches) == 2


def test_unmatched_newbies_do_notforcefully_match_top_players(player_factory):
def test_newbie_team_matched_with_pro_team(player_factory):
newbie = Search([
player_factory(1500, 500, ladder_games=0),
player_factory(1500, 500, ladder_games=0)
])
pro = Search([
player_factory(1400, 10, ladder_games=100),
player_factory(1400, 10, ladder_games=100)
])

searches = [newbie, pro]
matches = algorithm.RandomlyMatchNewbies().find(searches)
# No match if the pros are on their first attempt
assert len(matches) == 0

pro.register_failed_matching_attempt()
matches = algorithm.RandomlyMatchNewbies().find(searches)
assert len(matches) == 2


def test_unmatched_newbies_do_not_forcefully_match_top_players(player_factory):
newbie = Search([player_factory(1500, 500, ladder_games=0)])
top_player = Search([player_factory(2500, 10, ladder_games=100)])
top_player.register_failed_matching_attempt()

searches = [newbie, top_player]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert len(matches) == 0


def test_unmatched_newbies_do_not_forcefully_match_teams(player_factory):
newbie = Search([player_factory(1500, 500, ladder_games=0)])
team = Search([player_factory(1500, 100), player_factory(1500, 100)])
def test_newbie_team_dos_not_match_with_top_players_team(player_factory):
newbie = Search([
player_factory(1500, 500, ladder_games=0),
player_factory(1500, 500, ladder_games=0)
])
top_player = Search([
player_factory(2500, 10, ladder_games=100),
player_factory(2500, 10, ladder_games=100)
])
top_player.register_failed_matching_attempt()

searches = [newbie, team]
searches = [newbie, top_player]
matches = algorithm.RandomlyMatchNewbies().find(searches)

assert len(matches) == 0
Expand All @@ -354,6 +439,7 @@ def unmatched_newbie_teams_do_not_forcefully_match_pros(player_factory):
player_factory(1500, 500, ladder_games=0)
])
pro = Search([player_factory(1800, 10, ladder_games=100)])
pro.register_failed_matching_attempt()

searches = [newbie_team, pro]
matches = algorithm.RandomlyMatchNewbies().find(searches)
Expand All @@ -366,6 +452,7 @@ def test_odd_number_of_unmatched_newbies(player_factory):
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.register_failed_matching_attempt()

searches = [newbie1, pro, newbie2, newbie3]
matches = algorithm.RandomlyMatchNewbies().find(searches)
Expand All @@ -379,10 +466,14 @@ def test_matchmaker(player_factory):
newbie_force_matched = Search([player_factory(200, 400, ladder_games=9)])

pro_that_matches1 = Search([player_factory(1800, 60, ladder_games=101)])
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.register_failed_matching_attempt()

top_player = Search([player_factory(2100, 50, ladder_games=200)])
top_player.register_failed_matching_attempt()

searches = [
newbie_that_matches1,
Expand Down

0 comments on commit 08d1d8f

Please sign in to comment.