diff --git a/server/config.py b/server/config.py index 75e2c89d3..433f7b676 100644 --- a/server/config.py +++ b/server/config.py @@ -137,6 +137,8 @@ def __init__(self): 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 + self.LADDER_NEWBIE_SEARCH_EXPANSION_MAX = 0.6 + self.LADDER_NEWBIE_SEARCH_EXPANSION_STEP = 0.1 # The method for choosing map pool rating # Can be "mean", "min", or "max" self.MAP_POOL_RATING_SELECTION = "mean" diff --git a/server/matchmaker/algorithm/random_newbies.py b/server/matchmaker/algorithm/random_newbies.py deleted file mode 100644 index 70cab50ed..000000000 --- a/server/matchmaker/algorithm/random_newbies.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Iterable - -from ..search import Match, Search -from .matchmaker import MatchmakingPolicy1v1 - - -class RandomlyMatchNewbies(MatchmakingPolicy1v1): - def find( - self, searches: Iterable[Search] - ) -> tuple[dict[Search, Search], list[Match]]: - self.matches.clear() - searches_remaining_unmatched = set(searches) - - unmatched_newbies: list[Search] = [] - first_opponent = None - for search in searches: - if search.has_high_rated_player(): - continue - elif 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) - searches_remaining_unmatched.discard(newbie1) - searches_remaining_unmatched.discard(newbie2) - - can_match_last_newbie_with_first_opponent = unmatched_newbies and first_opponent - if can_match_last_newbie_with_first_opponent: - newbie = unmatched_newbies[0] - self._match(newbie, first_opponent) - searches_remaining_unmatched.discard(newbie) - searches_remaining_unmatched.discard(first_opponent) - - return self.matches, list(searches_remaining_unmatched) diff --git a/server/matchmaker/algorithm/stable_marriage.py b/server/matchmaker/algorithm/stable_marriage.py index fa3f0603d..e2b351132 100644 --- a/server/matchmaker/algorithm/stable_marriage.py +++ b/server/matchmaker/algorithm/stable_marriage.py @@ -6,7 +6,6 @@ from ...decorators import with_logger from ..search import Match, Search from .matchmaker import Matchmaker, MatchmakingPolicy1v1 -from .random_newbies import RandomlyMatchNewbies WeightedGraph = dict[Search, list[tuple[Search, float]]] @@ -100,13 +99,9 @@ def find( _MatchingGraph.remove_isolated(ranks) matches.update(StableMarriage().find(ranks)) - remaining_searches = [ + unmatched_searches = [ search for search in searches if search not in matches ] - self._logger.debug("Matching randomly for remaining newbies...") - - randomly_matched_newbies, unmatched_searches = RandomlyMatchNewbies().find(remaining_searches) - matches.update(randomly_matched_newbies) return self._remove_duplicates(matches), unmatched_searches diff --git a/server/matchmaker/search.py b/server/matchmaker/search.py index e43c65035..962b1b98b 100644 --- a/server/matchmaker/search.py +++ b/server/matchmaker/search.py @@ -151,6 +151,11 @@ def search_expansion(self) -> float: 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, @@ -257,7 +262,7 @@ def cancel(self): def __str__(self) -> str: return ( f"Search({self.rating_type}, {self._players_repr()}, threshold=" - f"{self.match_threshold:.2}, expansion={self.search_expansion:.2})" + f"{self.match_threshold:.2f}, expansion={self.search_expansion:.2f})" ) def _players_repr(self) -> str: diff --git a/tests/unit_tests/test_matchmaker_algorithm_stable_marriage.py b/tests/unit_tests/test_matchmaker_algorithm_stable_marriage.py index 44ab6408b..bc35c4cf7 100644 --- a/tests/unit_tests/test_matchmaker_algorithm_stable_marriage.py +++ b/tests/unit_tests/test_matchmaker_algorithm_stable_marriage.py @@ -225,233 +225,137 @@ def test_stable_marriage_matches_new_players_with_new_and_old_with_old_if_same_m assert matches[old1] == old2 -def test_stable_marriage_better_than_greedy(player_factory): - s1 = Search([player_factory(2300, 64, name="p1")]) - s2 = Search([player_factory(2000, 64, name="p2")]) - s3 = Search([player_factory(2100, 64, name="p3")]) - s4 = Search([player_factory(2200, 64, name="p4")]) - s5 = Search([player_factory(2300, 64, name="p5")]) - s6 = Search([player_factory(2400, 64, name="p6")]) - - searches = [s1, s2, s3, s4, s5, s6] - ranks = stable_marriage._MatchingGraph.build_full(searches) - - matches = stable_marriage.StableMarriage().find(ranks) - - # Note that the most balanced configuration would be - # (s1, s6) quality: 0.93 - # (s2, s3) quality: 0.93 - # (s4, s5) quality: 0.93 - - # However, because s1 is first in the list and gets top choice, we end with - # the following stable configuration - assert matches[s1] == s5 # quality: 0.97 - assert matches[s2] == s3 # quality: 0.93 - assert matches[s4] == s6 # quality: 0.82 - - -def test_stable_marriage_unmatch(player_factory): - s1 = Search([player_factory(503, 64, name="p1")]) - s2 = Search([player_factory(504, 64, name="p2")]) - s3 = Search([player_factory(504, 64, name="p3")]) - s4 = Search([player_factory(505, 64, name="p4")]) - - searches = [s1, s2, s3, s4] - ranks = stable_marriage._MatchingGraph.build_full(searches) +def test_newbies_with_big_rating_difference_are_matched_after_failed_matching(player_factory): + new1 = Search([player_factory(1500, 500, name="new1", ladder_games=0)]) + # this is the lower boundary of the typical rating ranges that people have after 10 games + new2 = Search([player_factory(100, 160, name="new2", ladder_games=config.NEWBIE_MIN_GAMES)]) - matches = stable_marriage.StableMarriage().find(ranks) + searches = [new1, new2] - assert matches[s1] == s4 # quality: 0.96622 - assert matches[s2] == s3 # quality: 0.96623 + team_size = 1 + matchmaker = stable_marriage.StableMarriageMatchmaker() + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) + assert len(matches) == 0 + assert len(unmatched_searches) == 2 -def test_random_newbie_matching_is_symmetric(player_factory): - s1 = Search([player_factory(1000, 500, name="p1", ladder_games=5)]) - s2 = Search([player_factory(1200, 500, name="p2", ladder_games=5)]) - s3 = Search([player_factory(900, 500, name="p3", ladder_games=5)]) - s4 = Search([player_factory(1500, 500, name="p4", ladder_games=5)]) - s5 = Search([player_factory(1700, 500, name="p5", ladder_games=5)]) - s6 = Search([player_factory(600, 500, name="p6", ladder_games=5)]) + for _ in range(6): + new1.register_failed_matching_attempt() + new2.register_failed_matching_attempt() - searches = [s1, s2, s3, s4, s5, s6] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) - assert len(matches) == len(searches) + assert len(matches) == 1 assert len(unmatched_searches) == 0 - for search in matches: - opponent = matches[search] - assert matches[opponent] == search - - -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, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) - - assert matches[newbie1] == newbie2 - assert matches[newbie2] == newbie1 - assert unmatched_searches == [pro] - -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) - ]) +def test_newbies_with_big_rating_difference_are_matched_after_failed_matching2(player_factory): + # keep the rating interpolation in mind + new1 = Search([player_factory(1500, 500, name="new1", ladder_games=0)]) + # this is the upper boundary of the typical rating ranges that people have after 10 games + new2 = Search([player_factory(1300, 160, name="new2", ladder_games=config.NEWBIE_MIN_GAMES)]) - searches = [newbie1, newbie2] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + searches = [new1, new2] - assert matches[newbie1] == newbie2 - assert matches[newbie2] == newbie1 - assert len(unmatched_searches) == 0 + team_size = 1 + matchmaker = stable_marriage.StableMarriageMatchmaker() + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) + assert len(matches) == 0 + assert len(unmatched_searches) == 2 -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) - ]) + for _ in range(6): + new1.register_failed_matching_attempt() + new2.register_failed_matching_attempt() - searches = [partial_newbie, newbie] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) - assert matches[partial_newbie] == newbie - assert matches[newbie] == partial_newbie + assert len(matches) == 1 assert len(unmatched_searches) == 0 -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) - ]) +def test_stable_marriage_does_not_match_new_and_old_players_with_big_rating_difference(player_factory): + new = Search([player_factory(1500, 500, name="new", ladder_games=0)]) + old = Search([player_factory(1000, 75, name="old", ladder_games=100)]) - searches = [newbie_and_top_rated, newbie] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + searches = [new, old] - assert not matches - assert len(unmatched_searches) == len(searches) - - -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, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) - # No match if the pro is on their first attempt - assert len(matches) == 0 - assert len(unmatched_searches) == 2 - - pro.register_failed_matching_attempt() - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) - assert len(matches) == 2 - assert len(unmatched_searches) == 0 + for _ in range(100): + new.register_failed_matching_attempt() + old.register_failed_matching_attempt() + team_size = 1 + matchmaker = stable_marriage.StableMarriageMatchmaker() + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) -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, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) - # No match if the pros are on their first attempt assert len(matches) == 0 assert len(unmatched_searches) == 2 - pro.register_failed_matching_attempt() - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) - assert len(matches) == 2 - assert len(unmatched_searches) == 0 +def test_newbies_are_matched_with_newbies(player_factory): + newbie1 = Search([player_factory(0, 500, ladder_games=9)]) + newbie2 = Search([player_factory(750, 500, ladder_games=9)]) + pro = Search([player_factory(1500, 70, ladder_games=100)]) -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, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + for _ in range(6): + newbie1.register_failed_matching_attempt() + newbie2.register_failed_matching_attempt() + pro.register_failed_matching_attempt() - assert len(matches) == 0 - assert len(unmatched_searches) == 2 + searches = [newbie1, pro, newbie2] + team_size = 1 + matchmaker = stable_marriage.StableMarriageMatchmaker() + matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) + match_sets = [set(pair) for pair in matches] + assert {newbie1, newbie2} in match_sets + assert unmatched_searches == [pro] -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, top_player] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) +def test_stable_marriage_better_than_greedy(player_factory): + s1 = Search([player_factory(2300, 64, name="p1")]) + s2 = Search([player_factory(2000, 64, name="p2")]) + s3 = Search([player_factory(2100, 64, name="p3")]) + s4 = Search([player_factory(2200, 64, name="p4")]) + s5 = Search([player_factory(2300, 64, name="p5")]) + s6 = Search([player_factory(2400, 64, name="p6")]) - assert len(matches) == 0 - assert len(unmatched_searches) == 2 + searches = [s1, s2, s3, s4, s5, s6] + ranks = stable_marriage._MatchingGraph.build_full(searches) + matches = stable_marriage.StableMarriage().find(ranks) -def unmatched_newbie_teams_do_not_forcefully_match_pros(player_factory): - newbie_team = Search([ - player_factory(1500, 500, ladder_games=0), - player_factory(1500, 500, ladder_games=0) - ]) - pro = Search([player_factory(1800, 10, ladder_games=100)]) - pro.register_failed_matching_attempt() + # Note that the most balanced configuration would be + # (s1, s6) quality: 0.93 + # (s2, s3) quality: 0.93 + # (s4, s5) quality: 0.93 - searches = [newbie_team, pro] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + # However, because s1 is first in the list and gets top choice, we end with + # the following stable configuration + assert matches[s1] == s5 # quality: 0.97 + assert matches[s2] == s3 # quality: 0.93 + assert matches[s4] == s6 # quality: 0.82 - assert len(matches) == 0 - assert len(unmatched_searches) == 2 +def test_stable_marriage_unmatch(player_factory): + s1 = Search([player_factory(503, 64, name="p1")]) + s2 = Search([player_factory(504, 64, name="p2")]) + s3 = Search([player_factory(504, 64, name="p3")]) + s4 = Search([player_factory(505, 64, name="p4")]) -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, 70, ladder_games=100)]) - pro.register_failed_matching_attempt() + searches = [s1, s2, s3, s4] + ranks = stable_marriage._MatchingGraph.build_full(searches) - searches = [newbie1, pro, newbie2, newbie3] - matches, unmatched_searches = stable_marriage.RandomlyMatchNewbies().find(searches) + matches = stable_marriage.StableMarriage().find(ranks) - assert len(matches) == 4 - assert len(unmatched_searches) == 0 + assert matches[s1] == s4 # quality: 0.96622 + assert matches[s2] == s3 # quality: 0.96623 def test_matchmaker(player_factory): newbie_that_matches1 = Search([player_factory(1450, 500, ladder_games=1)]) newbie_that_matches2 = Search([player_factory(1550, 500, ladder_games=1)]) - newbie_force_matched = Search([player_factory(200, 400, ladder_games=9)]) + newbie_alone = 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() @@ -466,7 +370,7 @@ def test_matchmaker(player_factory): searches = [ newbie_that_matches1, newbie_that_matches2, - newbie_force_matched, + newbie_alone, pro_that_matches1, pro_that_matches2, pro_alone, @@ -479,9 +383,11 @@ def test_matchmaker(player_factory): assert {newbie_that_matches1, newbie_that_matches2} in match_sets assert {pro_that_matches1, pro_that_matches2} in match_sets - assert {newbie_force_matched, pro_alone} in match_sets - assert unmatched_searches == [top_player] + assert {newbie_alone, pro_alone} not in match_sets + assert set(unmatched_searches) == {newbie_alone, pro_alone, top_player} for match_pair in match_pairs: + assert newbie_alone not in match_pair + assert pro_alone not in match_pair assert top_player not in match_pair @@ -500,23 +406,9 @@ def test_matchmaker_performance(player_factory, bench, caplog): assert bench.elapsed() < 0.5 -def test_matchmaker_random_only(player_factory): - newbie1 = Search([player_factory(1550, 500, ladder_games=1)]) - newbie2 = Search([player_factory(200, 400, ladder_games=9)]) - - searches = (newbie1, newbie2) - team_size = 1 - matchmaker = stable_marriage.StableMarriageMatchmaker() - match_pairs, unmatched_searches = matchmaker.find(searches, team_size, 1000) - match_sets = [set(pair) for pair in match_pairs] - - assert {newbie1, newbie2} in match_sets - assert len(unmatched_searches) == 0 - - def test_find_will_not_match_low_quality_games(player_factory): - s1 = Search([player_factory(100, 64, name="p1")]) - s2 = Search([player_factory(2000, 64, name="p2")]) + s1 = Search([player_factory(1000, 64, name="p1")]) + s2 = Search([player_factory(1300, 64, name="p2")]) searches = [s1, s2] @@ -530,14 +422,14 @@ def test_find_will_not_match_low_quality_games(player_factory): def test_unmatched_searches_without_newbies(player_factory): players = [ - player_factory(100, 10, name="lowRating_unmatched_1"), - player_factory(500, 10, name="lowRating_unmatched_2"), - player_factory(1500, 10, name="midRating_matched_1"), - player_factory(1500, 10, name="midRating_matched_2"), - player_factory(1500, 10, name="midRating_matched_3"), - player_factory(1500, 10, name="midRating_matched_4"), - player_factory(2000, 10, name="highRating_unmatched_1"), - player_factory(2500, 10, name="highRating_unmatched_2"), + player_factory(100, 70, name="lowRating_unmatched_1"), + player_factory(500, 70, name="lowRating_unmatched_2"), + player_factory(1500, 70, name="midRating_matched_1"), + player_factory(1500, 70, name="midRating_matched_2"), + player_factory(1500, 70, name="midRating_matched_3"), + player_factory(1500, 70, name="midRating_matched_4"), + player_factory(2000, 70, name="highRating_unmatched_1"), + player_factory(2500, 70, name="highRating_unmatched_2"), ] searches = [Search([player]) for player in players] @@ -552,29 +444,25 @@ def test_unmatched_searches_without_newbies(player_factory): def test_unmatched_searches_with_newbies(player_factory): players = [ - player_factory(100, 10, name="newbie1", ladder_games=1), - player_factory(200, 10, name="newbie2", ladder_games=1), - player_factory(300, 10, name="newbie3", ladder_games=1), - player_factory(400, 10, name="newbie4", ladder_games=1), - player_factory(500, 10, name="newbie5", ladder_games=1), - player_factory(1500, 10, name="midRating_matched_1"), - player_factory(1500, 10, name="midRating_matched_2"), - player_factory(1500, 10, name="midRating_matched_3"), - player_factory(1500, 10, name="midRating_matched_4"), - player_factory(2000, 10, name="highRating_unmatched_1"), - player_factory(2500, 10, name="highRating_unmatched_2"), + player_factory(100, 70, name="newbie1", ladder_games=1), + player_factory(200, 70, name="newbie2", ladder_games=1), + player_factory(300, 70, name="newbie3", ladder_games=1), + player_factory(400, 70, name="newbie4", ladder_games=1), + player_factory(500, 70, name="newbie5", ladder_games=1), + player_factory(750, 70, name="lowRating_unmatched_1"), + player_factory(1500, 70, name="midRating_matched_1"), + player_factory(1500, 70, name="midRating_matched_2"), + player_factory(1500, 70, name="midRating_matched_3"), + player_factory(1500, 70, name="midRating_matched_4"), + player_factory(2000, 70, name="highRating_unmatched_1"), + player_factory(2500, 70, name="highRating_unmatched_2"), ] searches = [Search([player]) for player in players] - force_matched_player = player_factory(750, 10, name="lowRating_unmatched_1") - force_matched_search = Search([force_matched_player]) - force_matched_search.register_failed_matching_attempt() - searches.append(force_matched_search) - team_size = 1 matchmaker = stable_marriage.StableMarriageMatchmaker() matches, unmatched_searches = matchmaker.find(searches, team_size, 1000) - expected_number_of_matches = 5 + expected_number_of_matches = 4 assert len(matches) == expected_number_of_matches assert len(unmatched_searches) == len(searches) - 2 * team_size * expected_number_of_matches diff --git a/tests/unit_tests/test_matchmaker_queue.py b/tests/unit_tests/test_matchmaker_queue.py index a80dc6d6e..db058bc11 100644 --- a/tests/unit_tests/test_matchmaker_queue.py +++ b/tests/unit_tests/test_matchmaker_queue.py @@ -90,20 +90,21 @@ def test_search_threshold(player_factory, rating): def test_search_threshold_of_single_old_players_is_high(player_factory): old_player = player_factory("experienced_player", ladder_rating=(1500, 50)) s = Search([old_player]) - assert s.match_threshold >= 0.6 + assert s.match_threshold >= 0.7 def test_search_threshold_of_team_old_players_is_high(player_factory): old_player = player_factory("experienced_player", ladder_rating=(1500, 50)) another_old_player = player_factory("another experienced_player", ladder_rating=(1600, 60)) s = Search([old_player, another_old_player]) - assert s.match_threshold >= 0.6 + assert s.match_threshold >= 0.7 def test_search_threshold_of_single_new_players_is_low(player_factory): new_player = player_factory("new_player", ladder_rating=(1500, 500), ladder_games=1) s = Search([new_player]) assert s.match_threshold <= 0.4 + assert s.match_threshold > 0.3 def test_search_threshold_of_team_new_players_is_low(player_factory): @@ -111,6 +112,23 @@ def test_search_threshold_of_team_new_players_is_low(player_factory): another_new_player = player_factory("another_new_player", ladder_rating=(1450, 450), ladder_games=1) s = Search([new_player, another_new_player]) assert s.match_threshold <= 0.4 + assert s.match_threshold > 0.3 + + +def test_search_threshold_of_single_not_so_new_players_is_higher(player_factory): + # This is roughly the deviation minimum after 10 games + new_player = player_factory("new_player", ladder_rating=(1500, 150), ladder_games=10) + s = Search([new_player]) + assert s.match_threshold <= 0.7 + assert s.match_threshold > 0.6 + + +def test_search_threshold_of_single_new_players_reaches_zero(player_factory): + new_player = player_factory("new_player", ladder_rating=(1500, 250), ladder_games=1) + s = Search([new_player]) + for _ in range(100): + s.register_failed_matching_attempt() + assert s.match_threshold == 0 @given(rating1=st_rating(), rating2=st_rating()) @@ -211,6 +229,25 @@ def test_search_expansion_for_top_players(matchmaker_players): assert e1 == config.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX +def test_search_expansion_for_newbies(matchmaker_players): + p1 = matchmaker_players[5] + s1 = Search([p1]) + + assert s1.search_expansion == 0.0 + + s1.register_failed_matching_attempt() + assert s1.search_expansion == config.LADDER_NEWBIE_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_NEWBIE_SEARCH_EXPANSION_MAX + + async def test_search_await(matchmaker_players): p1, p2, _, _, _, _ = matchmaker_players s1, s2 = Search([p1]), Search([p2])