Skip to content

Commit 7e1dcb6

Browse files
committed
Add logic to handle EstablishedPeer messages
1 parent aeeb4c2 commit 7e1dcb6

File tree

5 files changed

+324
-3
lines changed

5 files changed

+324
-3
lines changed

server/game_connection_matrix.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from collections import defaultdict
2+
3+
4+
class ConnectionMatrix:
5+
def __init__(self, established_peers: dict[int, set[int]]):
6+
self.established_peers = established_peers
7+
8+
def get_unconnected_peer_ids(self) -> set[int]:
9+
unconnected_peer_ids: set[int] = set()
10+
11+
# Group players by number of connected peers
12+
players_by_num_peers = defaultdict(list)
13+
for player_id, peer_ids in self.established_peers.items():
14+
players_by_num_peers[len(peer_ids)].append((player_id, peer_ids))
15+
16+
# Mark players with least number of connections as unconnected if they
17+
# don't meet the connection threshold. Each time a player is marked as
18+
# 'unconnected', remaining players need 1 less connection to be
19+
# considered connected.
20+
connected_peers = dict(self.established_peers)
21+
for num_connected, peers in sorted(players_by_num_peers.items()):
22+
if num_connected < len(connected_peers) - 1:
23+
for player_id, peer_ids in peers:
24+
unconnected_peer_ids.add(player_id)
25+
del connected_peers[player_id]
26+
27+
return unconnected_peer_ids

server/gameconnection.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import contextlib
77
import json
88
import logging
9-
from typing import Any
9+
from typing import Any, Optional
1010

1111
from sqlalchemy import select
1212

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

6670
self.setup_timeout = setup_timeout
6771

@@ -568,7 +572,16 @@ async def handle_established_peers(
568572
established a connection to. Is a list stored in a string, separated
569573
by spaces. As an example: "1321 22 33 43221 2132"
570574
"""
571-
pass
575+
if self.established_peer_ids is None:
576+
self.established_peer_ids = set()
577+
578+
# TODO: Is it possible for a previously connected player to disconnect?
579+
# How is the server notified when that happens?
580+
self.established_peer_ids.add(int(peer_id))
581+
582+
# TODO: We could capture all reported peer statuses here and
583+
# reconcile them similar to how we do with game results in order to
584+
# detect discrepancies between what players are reporting.
572585

573586
def _mark_dirty(self):
574587
if self.game:

server/games/game.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
game_stats,
1818
matchmaker_queue_game
1919
)
20+
from server.game_connection_matrix import ConnectionMatrix
2021
from server.games.game_results import (
2122
ArmyOutcome,
2223
ArmyReportedOutcome,
@@ -211,13 +212,41 @@ def players(self) -> list[Player]:
211212

212213
def get_connected_players(self) -> list[Player]:
213214
"""
214-
Get a collection of all players currently connected to the game.
215+
Get a collection of all players currently connected to the host.
215216
"""
216217
return [
217218
player for player in self._connections.keys()
218219
if player.id in self._configured_player_ids
219220
]
220221

222+
def get_unconnected_players_from_peer_matrix(
223+
self,
224+
) -> Optional[list[Player]]:
225+
"""
226+
Get a list of players who are not fully connected to the game based on
227+
the established peers matrix if possible. The EstablishedPeers messages
228+
might not be implemented by the game in which case this returns None.
229+
"""
230+
if any(
231+
conn.established_peer_ids is None
232+
for conn in self._connections.values()
233+
):
234+
return None
235+
236+
matrix = ConnectionMatrix(
237+
established_peers={
238+
player.id: conn.established_peer_ids
239+
for player, conn in self._connections.items()
240+
}
241+
)
242+
unconnected_peer_ids = matrix.get_unconnected_peer_ids()
243+
244+
return [
245+
player
246+
for player in self._connections.keys()
247+
if player.id in unconnected_peer_ids
248+
]
249+
221250
def _is_observer(self, player: Player) -> bool:
222251
army = self.get_player_option(player.id, "Army")
223252
return army is None or army < 0

server/ladder_service/ladder_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,12 @@ async def launch_match(
667667
try:
668668
await game.wait_launched(60 + 10 * len(guests))
669669
except asyncio.TimeoutError:
670+
unconnected_players = game.get_unconnected_players_from_peer_matrix()
671+
if unconnected_players is not None:
672+
raise NotConnectedError(unconnected_players)
673+
674+
# If the connection matrix was not available, fall back to looking
675+
# at who was connected to the host only.
670676
connected_players = game.get_connected_players()
671677
raise NotConnectedError([
672678
player for player in guests
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
from server.game_connection_matrix import ConnectionMatrix
2+
3+
4+
def test_all_connected():
5+
# One by hand example
6+
matrix = ConnectionMatrix(
7+
established_peers={
8+
0: {1, 2, 3},
9+
1: {0, 2, 3},
10+
2: {0, 1, 3},
11+
3: {0, 1, 2},
12+
},
13+
)
14+
assert matrix.get_unconnected_peer_ids() == set()
15+
16+
# Check every fully connected grid, including the empty grid
17+
for num_players in range(0, 16 + 1):
18+
matrix = ConnectionMatrix(
19+
established_peers={
20+
player_id: {
21+
peer_id
22+
for peer_id in range(num_players)
23+
if peer_id != player_id
24+
}
25+
for player_id in range(num_players)
26+
},
27+
)
28+
assert matrix.get_unconnected_peer_ids() == set()
29+
30+
31+
def test_1v1_not_connected():
32+
matrix = ConnectionMatrix(
33+
established_peers={
34+
0: set(),
35+
1: set(),
36+
},
37+
)
38+
assert matrix.get_unconnected_peer_ids() == {0, 1}
39+
40+
41+
def test_2v2_one_player_not_connected():
42+
# 0 is not connected to anyone
43+
matrix = ConnectionMatrix(
44+
established_peers={
45+
0: set(),
46+
1: {2, 3},
47+
2: {1, 3},
48+
3: {1, 2},
49+
},
50+
)
51+
assert matrix.get_unconnected_peer_ids() == {0}
52+
53+
54+
def test_2v2_two_players_not_connected():
55+
# 0 is not connected to anyone
56+
# 1 is not connected to anyone
57+
matrix = ConnectionMatrix(
58+
established_peers={
59+
0: set(),
60+
1: set(),
61+
2: {3},
62+
3: {2},
63+
},
64+
)
65+
assert matrix.get_unconnected_peer_ids() == {0, 1}
66+
67+
68+
def test_2v2_not_connected():
69+
# Not possible for only 3 players to be completely disconnected in a 4
70+
# player game. Either 1, 2, or all can be disconnected.
71+
matrix = ConnectionMatrix(
72+
established_peers={
73+
0: set(),
74+
1: set(),
75+
2: set(),
76+
3: set(),
77+
},
78+
)
79+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}
80+
81+
82+
def test_2v2_one_pair_not_connected():
83+
# 0 and 1 are not connected to each other
84+
matrix = ConnectionMatrix(
85+
established_peers={
86+
0: {2, 3},
87+
1: {2, 3},
88+
2: {0, 1, 3},
89+
3: {0, 1, 2},
90+
},
91+
)
92+
assert matrix.get_unconnected_peer_ids() == {0, 1}
93+
94+
95+
def test_2v2_two_pairs_not_connected():
96+
# 0 and 1 are not connected to each other
97+
# 1 and 2 are not connected to each other
98+
matrix = ConnectionMatrix(
99+
established_peers={
100+
0: {2, 3},
101+
1: {3},
102+
2: {0, 3},
103+
3: {0, 1, 2},
104+
},
105+
)
106+
assert matrix.get_unconnected_peer_ids() == {1}
107+
108+
109+
def test_2v2_two_disjoint_pairs_not_connected():
110+
# 0 and 1 are not connected to each other
111+
# 2 and 3 are not connected to each other
112+
matrix = ConnectionMatrix(
113+
established_peers={
114+
0: {2, 3},
115+
1: {2, 3},
116+
2: {0, 1},
117+
3: {0, 1},
118+
},
119+
)
120+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}
121+
122+
123+
def test_2v2_three_pairs_not_connected():
124+
# 0 and 1 are not connected to each other
125+
# 1 and 2 are not connected to each other
126+
# 2 and 3 are not connected to each other
127+
matrix = ConnectionMatrix(
128+
established_peers={
129+
0: {2, 3},
130+
1: {3},
131+
2: {0},
132+
3: {0, 1},
133+
},
134+
)
135+
assert matrix.get_unconnected_peer_ids() == {1, 2}
136+
137+
138+
def test_3v3_one_player_not_connected():
139+
# 0 is not connected to anyone
140+
matrix = ConnectionMatrix(
141+
established_peers={
142+
0: set(),
143+
1: {2, 3, 4, 5},
144+
2: {1, 3, 4, 5},
145+
3: {1, 2, 4, 5},
146+
4: {1, 2, 3, 5},
147+
5: {1, 2, 3, 4},
148+
},
149+
)
150+
assert matrix.get_unconnected_peer_ids() == {0}
151+
152+
153+
def test_3v3_two_players_not_connected():
154+
# 0 is not connected to anyone
155+
# 1 is not connected to anyone
156+
matrix = ConnectionMatrix(
157+
established_peers={
158+
0: set(),
159+
1: set(),
160+
2: {3, 4, 5},
161+
3: {2, 4, 5},
162+
4: {2, 3, 5},
163+
5: {2, 3, 4},
164+
},
165+
)
166+
assert matrix.get_unconnected_peer_ids() == {0, 1}
167+
168+
169+
def test_3v3_three_players_not_connected():
170+
# 0 is not connected to anyone
171+
# 1 is not connected to anyone
172+
# 2 is not connected to anyone
173+
matrix = ConnectionMatrix(
174+
established_peers={
175+
0: set(),
176+
1: set(),
177+
2: set(),
178+
3: {4, 5},
179+
4: {3, 5},
180+
5: {3, 4},
181+
},
182+
)
183+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2}
184+
185+
186+
def test_3v3_four_players_not_connected():
187+
# 0 is not connected to anyone
188+
# 1 is not connected to anyone
189+
# 2 is not connected to anyone
190+
# 3 is not connected to anyone
191+
matrix = ConnectionMatrix(
192+
established_peers={
193+
0: set(),
194+
1: set(),
195+
2: set(),
196+
3: set(),
197+
4: {5},
198+
5: {4},
199+
},
200+
)
201+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3}
202+
203+
204+
def test_3v3_not_connected():
205+
matrix = ConnectionMatrix(
206+
established_peers={
207+
0: set(),
208+
1: set(),
209+
2: set(),
210+
3: set(),
211+
4: set(),
212+
5: set(),
213+
},
214+
)
215+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2, 3, 4, 5}
216+
217+
218+
def test_3v3_one_pair_not_connected():
219+
# 0 and 1 are not connected to each other
220+
matrix = ConnectionMatrix(
221+
established_peers={
222+
0: {2, 3, 4, 5},
223+
1: {2, 3, 4, 5},
224+
2: {0, 1, 3, 4, 5},
225+
3: {0, 1, 2, 4, 5},
226+
4: {0, 1, 2, 3, 5},
227+
5: {0, 1, 2, 3, 4},
228+
},
229+
)
230+
assert matrix.get_unconnected_peer_ids() == {0, 1}
231+
232+
233+
def test_3v3_one_player_and_one_pair_not_connected():
234+
# 0 is not connected to anyone
235+
# 1 and 2 are not connected to each other
236+
matrix = ConnectionMatrix(
237+
established_peers={
238+
0: set(),
239+
1: {3, 4, 5},
240+
2: {3, 4, 5},
241+
3: {1, 2, 4, 5},
242+
4: {1, 2, 3, 5},
243+
5: {1, 2, 3, 4},
244+
},
245+
)
246+
assert matrix.get_unconnected_peer_ids() == {0, 1, 2}

0 commit comments

Comments
 (0)