Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#1032 working veto system #1033

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions server/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,13 @@

matchmaker_queue_map_pool = Table(
"matchmaker_queue_map_pool", metadata,
Column("matchmaker_queue_id", Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
Column("map_pool_id", Integer, ForeignKey("map_pool.id"), nullable=False),
Column("min_rating", Integer),
Column("max_rating", Integer),
Column("matchmaker_queue_id", Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
Column("map_pool_id", Integer, ForeignKey("map_pool.id"), nullable=False),
Column("min_rating", Integer),
Column("max_rating", Integer),
Column("veto_tokens_per_player", Integer, nullable=False),
Column("max_tokens_per_map", Integer, nullable=False),
Column("minimum_maps_after_veto", Float, nullable=False),
)

teamkills = Table(
Expand Down
57 changes: 53 additions & 4 deletions server/ladder_service/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
queue.team_size = info["team_size"]
queue.rating_peak = await self.fetch_rating_peak(info["rating_type"])
queue.map_pools.clear()
for map_pool_id, min_rating, max_rating in info["map_pools"]:
for map_pool_id, min_rating, max_rating, veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto in info["map_pools"]:
map_pool_name, map_list = map_pool_maps[map_pool_id]
if not map_list:
self._logger.warning(
Expand All @@ -112,7 +112,10 @@
queue.add_map_pool(
MapPool(map_pool_id, map_pool_name, map_list),
min_rating,
max_rating
max_rating,
veto_tokens_per_player,

Check warning on line 116 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

trailing whitespace

Check notice on line 116 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L116

Trailing whitespace
max_tokens_per_map,

Check warning on line 117 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

trailing whitespace

Check notice on line 117 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L117

Trailing whitespace
minimum_maps_after_veto
)
# Remove queues that don't exist anymore
for queue_name in list(self.queues.keys()):
Expand All @@ -125,6 +128,7 @@
select(
map_pool.c.id,
map_pool.c.name,
map_pool_map_version.c.id.label("map_pool_map_version_id"),
map_pool_map_version.c.weight,
map_pool_map_version.c.map_params,
map_version.c.id.label("map_id"),
Expand All @@ -150,6 +154,7 @@
map_list.append(
Map(
id=row.map_id,
map_pool_map_version_id=row.map_pool_map_version_id,
folder_name=folder_name,
ranked=row.ranked,
weight=row.weight,
Expand Down Expand Up @@ -191,6 +196,9 @@
matchmaker_queue_map_pool.c.map_pool_id,
matchmaker_queue_map_pool.c.min_rating,
matchmaker_queue_map_pool.c.max_rating,
matchmaker_queue_map_pool.c.veto_tokens_per_player,
matchmaker_queue_map_pool.c.max_tokens_per_map,
matchmaker_queue_map_pool.c.minimum_maps_after_veto,
game_featuredMods.c.gamemod,
leaderboard.c.technical_name.label("rating_type")
)
Expand Down Expand Up @@ -219,7 +227,10 @@
info["map_pools"].append((
row.map_pool_id,
row.min_rating,
row.max_rating
row.max_rating,
row.veto_tokens_per_player,
row.max_tokens_per_map,
row.minimum_maps_after_veto
))
except Exception:
self._logger.warning(
Expand Down Expand Up @@ -523,7 +534,25 @@
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)

Check warning on line 537 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

blank line contains whitespace

Check notice on line 537 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L537

Trailing whitespace
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():

Check warning on line 543 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L543

Redefining built-in 'id'

Check warning on line 543 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L543

Unused variable 'id'
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!")
Comment on lines +549 to +550
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not do validation of the number of tokens per player when starting the game. This validation and error raising should happen when the server receives the set_vetoes message. This is because as it is now a player could send the server more than the max amount of vetoes and then cause a game to never be able to launch.


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,
Expand Down Expand Up @@ -673,6 +702,26 @@
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

Check failure on line 710 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

multiple statements on one line (semicolon)

Check failure on line 710 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

multiple statements on one line (semicolon)
while (index < len(sorted_tokens)):

Check warning on line 711 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

trailing whitespace

Check notice on line 711 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L711

Trailing whitespace
(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):

Check warning on line 715 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

trailing whitespace

Check notice on line 715 in server/ladder_service/ladder_service.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/ladder_service/ladder_service.py#L715

Trailing whitespace
continue

Check warning on line 717 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

blank line contains whitespace
result = sum(sorted_tokens[:index]) / divider
upperLimit = sorted_tokens[index] if index < len(sorted_tokens) else float('inf')

Check warning on line 719 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

Single quotes found but double quotes preferred
if (result <= upperLimit):
return result

Check warning on line 722 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

blank line contains whitespace
raise Exception("Failed to calculate dynamic tokens per map: impossible vetoes setup")
Comment on lines +705 to +723
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here we are throwing an exception based on user input so a user could craft a veto selection that could cause the majority of games to fail to start. It would be better to have some default value rather than throwing an exception.


Check warning on line 724 in server/ladder_service/ladder_service.py

View workflow job for this annotation

GitHub Actions / flake8

blank line contains whitespace
async def get_game_history(
self,
players: list[Player],
Expand Down
4 changes: 4 additions & 0 deletions server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,10 @@

self.party_service.set_factions(self.player, list(factions))

async def command_set_player_vetoes(self, message):
converted = {v['map_pool_map_version_id']: v['veto_tokens_applied'] for v in message["vetoes"]}

Check failure on line 1319 in server/lobbyconnection.py

View workflow job for this annotation

GitHub Actions / flake8

multiple spaces after operator

Check warning on line 1319 in server/lobbyconnection.py

View workflow job for this annotation

GitHub Actions / flake8

Single quotes found but double quotes preferred
self.player.vetoes = converted

async def send_warning(self, message: str, fatal: bool = False):
"""
Display a warning message to the client
Expand Down
22 changes: 15 additions & 7 deletions server/matchmaker/map_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
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:

Check failure on line 24 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

unexpected spaces around keyword / parameter equals

Check failure on line 24 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

unexpected spaces around keyword / parameter equals

Check failure on line 24 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

unexpected spaces around keyword / parameter equals

Check failure on line 24 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

unexpected spaces around keyword / parameter equals
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(
Expand All @@ -50,10 +52,16 @@
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()

Check notice on line 56 in server/matchmaker/map_pool.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/matchmaker/map_pool.py#L56

Trailing whitespace
least_common_ids = {id_ for id_, _ in least_common}

Check failure on line 57 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

too many blank lines (3)

Check notice on line 57 in server/matchmaker/map_pool.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/matchmaker/map_pool.py#L57

Trailing whitespace

#multiply weight by 2 if map is least common and not vetoed by anyone

Check failure on line 59 in server/matchmaker/map_pool.py

View workflow job for this annotation

GitHub Actions / flake8

block comment should start with '# '
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]

Check warning on line 63 in server/matchmaker/map_pool.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/matchmaker/map_pool.py#L63

Redefining built-in 'map'

Check warning on line 63 in server/matchmaker/map_pool.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/matchmaker/map_pool.py#L63

Standard pseudo-random generators are not suitable for security/cryptographic purposes.
return map

def __repr__(self) -> str:
return f"MapPool({self.id}, {self.name}, {list(self.maps.values())})"
11 changes: 7 additions & 4 deletions server/matchmaker/matchmaker_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(
rating_type: str,
team_size: int = 1,
params: Optional[dict[str, Any]] = None,
map_pools: Iterable[tuple[MapPool, Optional[int], Optional[int]]] = (),
map_pools: Iterable[tuple[MapPool, Optional[int], Optional[int], int, int, float]] = (),
):
self.game_service = game_service
self.name = name
Expand Down Expand Up @@ -78,12 +78,15 @@ def add_map_pool(
self,
map_pool: MapPool,
min_rating: Optional[int],
max_rating: Optional[int]
max_rating: Optional[int],
veto_tokens_per_player: int,
max_tokens_per_map: int,
minimum_maps_after_veto: float,
) -> None:
self.map_pools[map_pool.id] = (map_pool, min_rating, max_rating)
self.map_pools[map_pool.id] = (map_pool, min_rating, max_rating, veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto)

def get_map_pool_for_rating(self, rating: float) -> Optional[MapPool]:
for map_pool, min_rating, max_rating in self.map_pools.values():
for map_pool, min_rating, max_rating, _, _, _ in self.map_pools.values():
if min_rating is not None and rating < min_rating:
continue
if max_rating is not None and rating > max_rating:
Expand Down
13 changes: 13 additions & 0 deletions server/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
lobby_connection: Optional["LobbyConnection"] = None
) -> None:
self._faction = Faction.uef
self._vetoes = {}

self.id = player_id
self.login = login
Expand Down Expand Up @@ -89,6 +90,18 @@
else:
self._faction = Faction.from_value(value)

@property
def vetoes(self) -> dict[int, int]:
return self._vetoes

@vetoes.setter
def vetoes(self, value: dict[int, int]) -> None:
if not isinstance(value, dict):
raise ValueError("Vetoes must be a dictionary")
if not all(isinstance(key, int) and isinstance(val, int) for key, val in value.items()):
raise ValueError("Vetoes dictionary must contain only integer keys and values")
self._vetoes = value

Check notice on line 103 in server/players.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/players.py#L103

Trailing whitespace

def power(self) -> int:
"""An artifact of the old permission system. The client still uses this
number to determine if a player gets a special category in the user list
Expand Down
3 changes: 3 additions & 0 deletions server/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def get_map(self) -> "Map": ...

class Map(NamedTuple):
id: Optional[int]
map_pool_map_version_id: Optional[int]
folder_name: str
ranked: bool = False
# Map pool only
Expand All @@ -60,6 +61,7 @@ def get_map(self) -> "Map":

class NeroxisGeneratedMap(NamedTuple):
id: int
map_pool_map_version_id: Optional[int]
version: str
spawns: int
map_size_pixels: int
Expand Down Expand Up @@ -137,6 +139,7 @@ def get_map(self) -> Map:
# the map argument in unit tests.
MAP_DEFAULT = Map(
id=None,
map_pool_map_version_id=None,
folder_name="scmp_007",
ranked=False,
)
Loading