diff --git a/server/ladder_service/ladder_service.py b/server/ladder_service/ladder_service.py index dedeb1093..477252568 100644 --- a/server/ladder_service/ladder_service.py +++ b/server/ladder_service/ladder_service.py @@ -534,7 +534,25 @@ def get_displayed_rating(player: Player) -> float: pool = queue.get_map_pool_for_rating(rating) if not pool: raise RuntimeError(f"No map pool available for rating {rating}!") - game_map = pool.choose_map(played_map_ids) + + pool, _, _, veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto = queue.map_pools[pool.id] + + vetoesMap = defaultdict(int) + tokensTotalPerPlayer = defaultdict(int) + + for (id, map) in pool.maps.items(): + for player in all_players: + vetoesMap[map.map_pool_map_version_id] += player.vetoes.get(map.map_pool_map_version_id, 0) + tokensTotalPerPlayer[player.id] += player.vetoes.get(map.map_pool_map_version_id, 0) + + for player in all_players: + if (tokensTotalPerPlayer[player.id] > veto_tokens_per_player): + raise RuntimeError(f"Player {player.id} has too many vetoes!") + + if (max_tokens_per_map == 0): + max_tokens_per_map = self.calculate_dynamic_tokens_per_map(minimum_maps_after_veto, vetoesMap.values()) + + game_map = pool.choose_map(played_map_ids, vetoesMap, max_tokens_per_map) game = self.game_service.create_game( game_class=LadderGame, @@ -684,6 +702,26 @@ async def launch_match( if player not in connected_players ]) + def calculate_dynamic_tokens_per_map(self, M: float, tokens: list[int]) -> float: + sorted_tokens = sorted(tokens) + if (sorted_tokens.count(0) >= M): + return 1 + + result = 1; last = 0; index = 0 + while (index < len(sorted_tokens)): + (index, last) = next(((i, el) for i, el in enumerate(sorted_tokens) if el > last), (len(sorted_tokens) - 1, sorted_tokens[-1])) + index += 1 + divider = index - M + if (divider <= 0): + continue + + result = sum(sorted_tokens[:index]) / divider + upperLimit = sorted_tokens[index] if index < len(sorted_tokens) else float('inf') + if (result <= upperLimit): + return result + + raise Exception("Failed to calculate dynamic tokens per map: impossible vetoes setup") + async def get_game_history( self, players: list[Player], diff --git a/server/matchmaker/map_pool.py b/server/matchmaker/map_pool.py index c74be0d7a..9067c8b1b 100644 --- a/server/matchmaker/map_pool.py +++ b/server/matchmaker/map_pool.py @@ -21,11 +21,13 @@ def __init__( def set_maps(self, maps: Iterable[MapPoolMap]) -> None: self.maps = {map_.id: map_ for map_ in maps} - def choose_map(self, played_map_ids: Iterable[int] = ()) -> Map: + def choose_map(self, played_map_ids: Iterable[int] = (), vetoesMap = None, max_tokens_per_map = 1) -> Map: + if vetoesMap is None: + vetoesMap = {} """ - Select a random map who's id does not appear in `played_map_ids`. If - all map ids appear in the list, then pick one that appears the least - amount of times. + Select a random map using veto system weights. + The maps which are least played from played_map_ids + and not vetoed by any player are getting x2 weight multiplier. """ if not self.maps: self._logger.critical( @@ -50,10 +52,16 @@ def choose_map(self, played_map_ids: Iterable[int] = ()) -> Map: least_common = least_common[:i] break - weights = [self.maps[id_].weight for id_, _ in least_common] - map_id = random.choices(least_common, weights=weights, k=1)[0][0] - return self.maps[map_id].get_map() + + least_common_ids = {id_ for id_, _ in least_common} + + #multiply weight by 2 if map is least common and not vetoed by anyone + mapList = list((map.map_pool_map_version_id, map, 2 if (map.id in least_common_ids) and (vetoesMap.get(map.map_pool_map_version_id) == 0) else 1) for id, map in self.maps.items()) + + weights = [max(0, (1 - vetoesMap.get(id, 0) / max_tokens_per_map) * map.weight * least_common_multiplier) for id, map, least_common_multiplier in mapList] + map = random.choices(mapList, weights=weights, k=1)[0][1] + return map def __repr__(self) -> str: return f"MapPool({self.id}, {self.name}, {list(self.maps.values())})"