Skip to content

Commit 0dbdc1a

Browse files
committed
Extract game launching sequence
1 parent eed2392 commit 0dbdc1a

2 files changed

Lines changed: 152 additions & 39 deletions

File tree

server/ladder_service.py

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,20 @@
3131
)
3232
from .decorators import with_logger
3333
from .game_service import GameService
34-
from .games import InitMode, LadderGame
34+
from .games import Game, InitMode, LadderGame
3535
from .matchmaker import MapPool, MatchmakerQueue, OnMatchedCallback, Search
3636
from .players import Player, PlayerState
37+
from .protocol import DisconnectedError
3738
from .types import GameLaunchOptions, Map, NeroxisGeneratedMap
3839

3940

41+
class GameLaunchError(Exception):
42+
"""When a game failed to launch because some players did not connect"""
43+
44+
def __init__(self, players: List[Player]):
45+
self.players = players
46+
47+
4048
@with_logger
4149
class LadderService(Service):
4250
"""
@@ -427,41 +435,7 @@ def get_player_mean(player):
427435
game.set_player_option(player.id, "Army", slot)
428436
game.set_player_option(player.id, "Color", slot)
429437

430-
self._logger.debug("Starting ladder game: %s", game)
431-
# Options shared by all players
432-
options = GameLaunchOptions(
433-
mapname=game.map_name,
434-
expected_players=len(all_players),
435-
)
436-
437-
def game_options(player: Player) -> GameLaunchOptions:
438-
return options._replace(
439-
team=game.get_player_option(player.id, "Team"),
440-
faction=player.faction,
441-
map_position=game.get_player_option(player.id, "StartSpot")
442-
)
443-
444-
await host.lobby_connection.launch_game(
445-
game, is_host=True, options=game_options(host)
446-
)
447-
try:
448-
await game.wait_hosted(60)
449-
finally:
450-
# TODO: Once the client supports `match_cancelled`, don't
451-
# send `launch_game` to the client if the host timed out. Until
452-
# then, failing to send `launch_game` will cause the client to
453-
# think it is searching for ladder, even though the server has
454-
# already removed it from the queue.
455-
456-
await asyncio.gather(*[
457-
guest.lobby_connection.launch_game(
458-
game, is_host=False, options=game_options(guest)
459-
)
460-
for guest in all_guests
461-
if guest.lobby_connection is not None
462-
])
463-
await game.wait_launched(60 + 10 * len(all_guests))
464-
self._logger.debug("Ladder game launched successfully")
438+
await self.launch_game(game, host, all_guests)
465439
except Exception:
466440
if game:
467441
await game.on_game_end()
@@ -473,6 +447,74 @@ def game_options(player: Player) -> GameLaunchOptions:
473447
player.state = PlayerState.IDLE
474448
player.write_message(msg)
475449

450+
async def launch_game(
451+
self,
452+
game: Game,
453+
host: Player,
454+
guests: List[Player]
455+
) -> None:
456+
self._logger.debug("Starting ladder game: %s", game)
457+
all_players = (host, *guests)
458+
459+
# Options shared by all players
460+
options = GameLaunchOptions(
461+
mapname=game.map_name,
462+
expected_players=len(all_players),
463+
)
464+
465+
def game_options(player: Player) -> GameLaunchOptions:
466+
return options._replace(
467+
team=game.get_player_option(player.id, "Team"),
468+
faction=player.faction,
469+
map_position=game.get_player_option(player.id, "StartSpot")
470+
)
471+
472+
# Check if anyone DC'd from the server entirely
473+
disconnected_players = [
474+
player for player in all_players
475+
if player.lobby_connection is None
476+
]
477+
if disconnected_players:
478+
raise GameLaunchError(disconnected_players)
479+
480+
try:
481+
await host.lobby_connection.launch_game(
482+
game,
483+
is_host=True,
484+
options=game_options(host)
485+
)
486+
await game.wait_hosted(60)
487+
except (asyncio.TimeoutError, DisconnectedError) as e:
488+
raise GameLaunchError([host]) from e
489+
finally:
490+
# TODO: Once the client supports `match_cancelled`, don't
491+
# send `launch_game` to the client if the host timed out. Until
492+
# then, failing to send `launch_game` will cause the client to
493+
# think it is searching for ladder, even though the server has
494+
# already removed it from the queue.
495+
496+
await asyncio.gather(*[
497+
guest.lobby_connection.launch_game(
498+
game,
499+
is_host=False,
500+
options=game_options(guest)
501+
)
502+
for guest in guests
503+
if guest.lobby_connection is not None
504+
], return_exceptions=True)
505+
506+
try:
507+
await game.wait_launched(60 + 10 * len(guests))
508+
except asyncio.TimeoutError as e:
509+
connected_players = game.players
510+
disconnected_players = [
511+
player for player in all_players
512+
if player not in connected_players
513+
]
514+
raise GameLaunchError(disconnected_players) from e
515+
516+
self._logger.debug("Ladder game launched successfully")
517+
476518
async def get_game_history(
477519
self,
478520
players: List[Player],

tests/unit_tests/test_ladder_service.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,35 @@
66
from hypothesis import given, settings
77
from hypothesis import strategies as st
88

9-
from server import LadderService, LobbyConnection
9+
from server import LobbyConnection
1010
from server.db.models import matchmaker_queue, matchmaker_queue_map_pool
11-
from server.games import LadderGame
12-
from server.ladder_service import game_name
11+
from server.games import GameState, LadderGame
12+
from server.ladder_service import GameLaunchError, LadderService, game_name
1313
from server.matchmaker import MapPool, MatchmakerQueue
1414
from server.players import PlayerState
15+
from server.protocol import DisconnectedError
1516
from server.rating import RatingType
1617
from server.types import Map, NeroxisGeneratedMap
1718
from tests.conftest import make_player
19+
from tests.unit_tests.conftest import add_connected_player
1820
from tests.utils import autocontext, fast_forward
1921

2022
from .strategies import st_players
2123

2224
pytestmark = pytest.mark.asyncio
2325

2426

27+
@pytest.fixture
28+
def game(database, game_service, game_stats_service):
29+
return LadderGame(
30+
42,
31+
database,
32+
game_service,
33+
game_stats_service,
34+
rating_type=RatingType.GLOBAL
35+
)
36+
37+
2538
async def test_queue_initialization(database, game_service):
2639
ladder_service = LadderService(database, game_service)
2740

@@ -176,6 +189,64 @@ async def test_start_game_timeout(
176189
# assert p2.state is PlayerState.IDLE
177190

178191

192+
async def test_launch_game_error_host(
193+
ladder_service: LadderService,
194+
game,
195+
player_factory
196+
):
197+
# Host disconnected before game launch
198+
host = player_factory("Dostya", player_id=1, lobby_connection_spec=None)
199+
guest = player_factory("Rhiza", player_id=2, lobby_connection_spec="auto")
200+
201+
with pytest.raises(GameLaunchError) as ex_info:
202+
await ladder_service.launch_game(game, host, [guest])
203+
assert ex_info.value.players == [host]
204+
205+
# Host disconnected during game launch
206+
host = player_factory("Dostya", player_id=1, lobby_connection_spec="auto")
207+
disconnected_error = DisconnectedError("Test")
208+
host.lobby_connection.launch_game.side_effect = disconnected_error
209+
210+
with pytest.raises(GameLaunchError) as ex_info:
211+
await ladder_service.launch_game(game, host, [guest])
212+
assert ex_info.value.players == [host]
213+
assert ex_info.value.__cause__ == disconnected_error
214+
215+
216+
async def test_launch_game_error_many_disconnected(
217+
ladder_service: LadderService,
218+
game,
219+
player_factory
220+
):
221+
# Multiple players disconnect before game launch
222+
host = player_factory("Dostya", player_id=1, lobby_connection_spec=None)
223+
p2 = player_factory("Rhiza", player_id=2, lobby_connection_spec="auto")
224+
p3 = player_factory("QAI", player_id=3, lobby_connection_spec=None)
225+
p4 = player_factory("Brackman", player_id=4, lobby_connection_spec="auto")
226+
227+
with pytest.raises(GameLaunchError) as ex_info:
228+
await ladder_service.launch_game(game, host, [p2, p3, p4])
229+
assert ex_info.value.players == [host, p3]
230+
231+
# Players 3 and 4 disconnect during game launch
232+
host = player_factory("Dostya", player_id=1, lobby_connection_spec="auto")
233+
p3 = player_factory("QAI", player_id=3, lobby_connection_spec="auto")
234+
disconnected_error = DisconnectedError("Test")
235+
timeout_error = asyncio.TimeoutError()
236+
p3.lobby_connection.launch_game.side_effect = disconnected_error
237+
p4.lobby_connection.launch_game.side_effect = disconnected_error
238+
game.wait_hosted = CoroutineMock()
239+
game.wait_launched = CoroutineMock(side_effect=timeout_error)
240+
game.state = GameState.LOBBY
241+
add_connected_player(game, host)
242+
add_connected_player(game, p2)
243+
244+
with pytest.raises(GameLaunchError) as ex_info:
245+
await ladder_service.launch_game(game, host, [p2, p3, p4])
246+
assert ex_info.value.players == [p3, p4]
247+
assert ex_info.value.__cause__ == timeout_error
248+
249+
179250
@given(
180251
player1=st_players("p1", player_id=1, lobby_connection_spec="mock"),
181252
player2=st_players("p2", player_id=2, lobby_connection_spec="mock"),

0 commit comments

Comments
 (0)