Skip to content

Commit

Permalink
Add logic to handle EstablishedPeer messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Askaholic committed Oct 26, 2024
1 parent aeeb4c2 commit af677ec
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 3 deletions.
22 changes: 22 additions & 0 deletions server/game_connection_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from collections import defaultdict


class ConnectionMatrix:
def __init__(self, established_peers: dict[int, set[int]]):
self.established_peers = established_peers

def get_unconnected_peer_ids(self) -> set[int]:
unconnected_peer_ids: set[int] = set()

players_by_num_peers = defaultdict(list)
for player_id, peer_ids in self.established_peers.items():
players_by_num_peers[len(peer_ids)].append((player_id, peer_ids))

connected_peers = dict(self.established_peers)
for num_connected, peers in sorted(players_by_num_peers.items()):
if num_connected < len(connected_peers) - 1:
for player_id, peer_ids in peers:
unconnected_peer_ids.add(player_id)
del connected_peers[player_id]

return unconnected_peer_ids
17 changes: 15 additions & 2 deletions server/gameconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import contextlib
import json
import logging
from typing import Any
from typing import Any, Optional

from sqlalchemy import select

Expand Down Expand Up @@ -62,6 +62,10 @@ def __init__(
self.player = player
player.game_connection = self # Set up weak reference to self
self.game = game
# None if the EstablishedPeers message is not implemented by the game
# version/mode used by the player. For instance, matchmaker might have
# it, but custom games might not.
self.established_peer_ids: Optional[set[int]] = None

self.setup_timeout = setup_timeout

Expand Down Expand Up @@ -568,7 +572,16 @@ async def handle_established_peers(
established a connection to. Is a list stored in a string, separated
by spaces. As an example: "1321 22 33 43221 2132"
"""
pass
if int(peer_id) != self.player.id:
# TODO: We could capture all reported peer statuses here and
# reconcile them similar to how we do with game results in order to
# detect discrepancies between what players are reporting.
return

Check warning on line 579 in server/gameconnection.py

View check run for this annotation

Codecov / codecov/patch

server/gameconnection.py#L579

Added line #L579 was not covered by tests

self.established_peer_ids = set(
int(peer_id)
for peer_id in peer_connected_to.split(" ")
)

def _mark_dirty(self):
if self.game:
Expand Down
31 changes: 30 additions & 1 deletion server/games/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
game_stats,
matchmaker_queue_game
)
from server.game_connection_matrix import ConnectionMatrix
from server.games.game_results import (
ArmyOutcome,
ArmyReportedOutcome,
Expand Down Expand Up @@ -211,13 +212,41 @@ def players(self) -> list[Player]:

def get_connected_players(self) -> list[Player]:
"""
Get a collection of all players currently connected to the game.
Get a collection of all players currently connected to the host.
"""
return [
player for player in self._connections.keys()
if player.id in self._configured_player_ids
]

def get_unconnected_players_from_peer_matrix(
self,
) -> Optional[list[Player]]:
"""
Get a list of players who are not fully connected to the game based on
the established peers matrix if possible. The EstablishedPeers messages
might not be implemented by the game in which case this returns None.
"""
if any(
conn.established_peer_ids is None
for conn in self._connections.values()
):
return None

matrix = ConnectionMatrix(
established_peers={
player.id: conn.established_peer_ids
for player, conn in self._connections.items()
}
)
unconnected_peer_ids = matrix.get_unconnected_peer_ids()

Check warning on line 242 in server/games/game.py

View check run for this annotation

Codecov / codecov/patch

server/games/game.py#L242

Added line #L242 was not covered by tests

return [
player
for player in self._connections.keys()
if player.id in unconnected_peer_ids
]

def _is_observer(self, player: Player) -> bool:
army = self.get_player_option(player.id, "Army")
return army is None or army < 0
Expand Down
6 changes: 6 additions & 0 deletions server/ladder_service/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,12 @@ async def launch_match(
try:
await game.wait_launched(60 + 10 * len(guests))
except asyncio.TimeoutError:
unconnected_players = game.get_unconnected_players_from_peer_matrix()
if unconnected_players is not None:
raise NotConnectedError(unconnected_players)

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

View check run for this annotation

Codecov / codecov/patch

server/ladder_service/ladder_service.py#L672

Added line #L672 was not covered by tests

# If the connection matrix was not available, fall back to looking
# at who was connected to the host only.
connected_players = game.get_connected_players()
raise NotConnectedError([
player for player in guests
Expand Down
246 changes: 246 additions & 0 deletions tests/unit_tests/test_connection_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
from server.game_connection_matrix import ConnectionMatrix


def test_all_connected():
# One by hand example
matrix = ConnectionMatrix(
established_peers={
0: {1, 2, 3},
1: {0, 2, 3},
2: {0, 1, 3},
3: {0, 1, 2},
},
)
assert matrix.get_unconnected_peer_ids() == set()

# Check every fully connected grid, including the empty grid
for num_players in range(0, 16 + 1):
matrix = ConnectionMatrix(
established_peers={
player_id: {
peer_id
for peer_id in range(num_players)
if peer_id != player_id
}
for player_id in range(num_players)
},
)
assert matrix.get_unconnected_peer_ids() == set()


def test_1v1_not_connected():
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1}


def test_2v2_one_player_not_connected():
# 0 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: {2, 3},
2: {1, 3},
3: {1, 2},
},
)
assert matrix.get_unconnected_peer_ids() == {0}


def test_2v2_two_players_not_connected():
# 0 is not connected to anyone
# 1 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: {3},
3: {2},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1}


def test_2v2_not_connected():
# Not possible for only 3 players to be completely disconnected in a 4
# player game. Either 1, 2, or all can be disconnected.
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: set(),
3: set(),
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}


def test_2v2_one_pair_not_connected():
# 0 and 1 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: {2, 3},
1: {2, 3},
2: {0, 1, 3},
3: {0, 1, 2},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1}


def test_2v2_two_pairs_not_connected():
# 0 and 1 are not connected to each other
# 1 and 2 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: {2, 3},
1: {3},
2: {0, 3},
3: {0, 1, 2},
},
)
assert matrix.get_unconnected_peer_ids() == {1}


def test_2v2_two_disjoint_pairs_not_connected():
# 0 and 1 are not connected to each other
# 2 and 3 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: {2, 3},
1: {2, 3},
2: {0, 1},
3: {0, 1},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}


def test_2v2_three_pairs_not_connected():
# 0 and 1 are not connected to each other
# 1 and 2 are not connected to each other
# 2 and 3 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: {2, 3},
1: {3},
2: {0},
3: {0, 1},
},
)
assert matrix.get_unconnected_peer_ids() == {1, 2}


def test_3v3_one_player_not_connected():
# 0 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: {2, 3, 4, 5},
2: {1, 3, 4, 5},
3: {1, 2, 4, 5},
4: {1, 2, 3, 5},
5: {1, 2, 3, 4},
},
)
assert matrix.get_unconnected_peer_ids() == {0}


def test_3v3_two_players_not_connected():
# 0 is not connected to anyone
# 1 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: {3, 4, 5},
3: {2, 4, 5},
4: {2, 3, 5},
5: {2, 3, 4},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1}


def test_3v3_three_players_not_connected():
# 0 is not connected to anyone
# 1 is not connected to anyone
# 2 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: set(),
3: {4, 5},
4: {3, 5},
5: {3, 4},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2}


def test_3v3_four_players_not_connected():
# 0 is not connected to anyone
# 1 is not connected to anyone
# 2 is not connected to anyone
# 3 is not connected to anyone
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: set(),
3: set(),
4: {5},
5: {4},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}


def test_3v3_not_connected():
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: set(),
2: set(),
3: set(),
4: set(),
5: set(),
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3, 4, 5}


def test_3v3_one_pair_not_connected():
# 0 and 1 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: {2, 3, 4, 5},
1: {2, 3, 4, 5},
2: {0, 1, 3, 4, 5},
3: {0, 1, 2, 4, 5},
4: {0, 1, 2, 3, 5},
5: {0, 1, 2, 3, 4},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1}


def test_3v3_one_player_and_one_pair_not_connected():
# 0 is not connected to anyone
# 1 and 2 are not connected to each other
matrix = ConnectionMatrix(
established_peers={
0: set(),
1: {3, 4, 5},
2: {3, 4, 5},
3: {1, 2, 4, 5},
4: {1, 2, 3, 5},
5: {1, 2, 3, 4},
},
)
assert matrix.get_unconnected_peer_ids() == {0, 1, 2}

0 comments on commit af677ec

Please sign in to comment.