Skip to content

Commit

Permalink
Extract game launching sequence
Browse files Browse the repository at this point in the history
  • Loading branch information
Askaholic committed May 7, 2021
1 parent 01d4cde commit 9de70f9
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 39 deletions.
114 changes: 78 additions & 36 deletions server/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,20 @@
)
from .decorators import with_logger
from .game_service import GameService
from .games import InitMode, LadderGame
from .games import Game, InitMode, LadderGame
from .matchmaker import MapPool, MatchmakerQueue, OnMatchedCallback, Search
from .players import Player, PlayerState
from .protocol import DisconnectedError
from .types import GameLaunchOptions, Map, NeroxisGeneratedMap


class GameLaunchError(Exception):
"""When a game failed to launch because some players did not connect"""

def __init__(self, players: List[Player]):
self.players = players


@with_logger
class LadderService(Service):
"""
Expand Down Expand Up @@ -427,41 +435,7 @@ def get_player_mean(player):
game.set_player_option(player.id, "Army", slot)
game.set_player_option(player.id, "Color", slot)

self._logger.debug("Starting ladder game: %s", game)
# Options shared by all players
options = GameLaunchOptions(
mapname=game.map_name,
expected_players=len(all_players),
)

def game_options(player: Player) -> GameLaunchOptions:
return options._replace(
team=game.get_player_option(player.id, "Team"),
faction=player.faction,
map_position=game.get_player_option(player.id, "StartSpot")
)

await host.lobby_connection.launch_game(
game, is_host=True, options=game_options(host)
)
try:
await game.wait_hosted(60)
finally:
# TODO: Once the client supports `match_cancelled`, don't
# send `launch_game` to the client if the host timed out. Until
# then, failing to send `launch_game` will cause the client to
# think it is searching for ladder, even though the server has
# already removed it from the queue.

await asyncio.gather(*[
guest.lobby_connection.launch_game(
game, is_host=False, options=game_options(guest)
)
for guest in all_guests
if guest.lobby_connection is not None
])
await game.wait_launched(60 + 10 * len(all_guests))
self._logger.debug("Ladder game launched successfully")
await self.launch_game(game, host, all_guests)
except Exception:
if game:
await game.on_game_end()
Expand All @@ -473,6 +447,74 @@ def game_options(player: Player) -> GameLaunchOptions:
player.state = PlayerState.IDLE
player.write_message(msg)

async def launch_game(
self,
game: Game,
host: Player,
guests: List[Player]
) -> None:
self._logger.debug("Starting ladder game: %s", game)
all_players = (host, *guests)

# Options shared by all players
options = GameLaunchOptions(
mapname=game.map_name,
expected_players=len(all_players),
)

def game_options(player: Player) -> GameLaunchOptions:
return options._replace(
team=game.get_player_option(player.id, "Team"),
faction=player.faction,
map_position=game.get_player_option(player.id, "StartSpot")
)

# Check if anyone DC'd from the server entirely
disconnected_players = [
player for player in all_players
if player.lobby_connection is None
]
if disconnected_players:
raise GameLaunchError(disconnected_players)

try:
await host.lobby_connection.launch_game(
game,
is_host=True,
options=game_options(host)
)
await game.wait_hosted(60)
except (asyncio.TimeoutError, DisconnectedError) as e:
raise GameLaunchError([host]) from e
finally:
# TODO: Once the client supports `match_cancelled`, don't
# send `launch_game` to the client if the host timed out. Until
# then, failing to send `launch_game` will cause the client to
# think it is searching for ladder, even though the server has
# already removed it from the queue.

await asyncio.gather(*[
guest.lobby_connection.launch_game(
game,
is_host=False,
options=game_options(guest)
)
for guest in guests
if guest.lobby_connection is not None
], return_exceptions=True)

try:
await game.wait_launched(60 + 10 * len(guests))
except asyncio.TimeoutError as e:
connected_players = game.players
disconnected_players = [
player for player in all_players
if player not in connected_players
]
raise GameLaunchError(disconnected_players) from e

self._logger.debug("Ladder game launched successfully")

async def get_game_history(
self,
players: List[Player],
Expand Down
77 changes: 74 additions & 3 deletions tests/unit_tests/test_ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,35 @@
from hypothesis import given, settings
from hypothesis import strategies as st

from server import LadderService, LobbyConnection
from server import LobbyConnection
from server.db.models import matchmaker_queue, matchmaker_queue_map_pool
from server.games import LadderGame
from server.ladder_service import game_name
from server.games import GameState, LadderGame
from server.ladder_service import GameLaunchError, LadderService, game_name
from server.matchmaker import MapPool, MatchmakerQueue
from server.players import PlayerState
from server.protocol import DisconnectedError
from server.rating import RatingType
from server.types import Map, NeroxisGeneratedMap
from tests.conftest import make_player
from tests.unit_tests.conftest import add_connected_player
from tests.utils import autocontext, fast_forward

from .strategies import st_players

pytestmark = pytest.mark.asyncio


@pytest.fixture
def game(database, game_service, game_stats_service):
return LadderGame(
42,
database,
game_service,
game_stats_service,
rating_type=RatingType.GLOBAL
)


async def test_queue_initialization(database, game_service):
ladder_service = LadderService(database, game_service)

Expand Down Expand Up @@ -170,6 +183,64 @@ async def test_start_game_timeout(
# assert p2.state is PlayerState.IDLE


async def test_launch_game_error_host(
ladder_service: LadderService,
game,
player_factory
):
# Host disconnected before game launch
host = player_factory("Dostya", player_id=1, lobby_connection_spec=None)
guest = player_factory("Rhiza", player_id=2, lobby_connection_spec="auto")

with pytest.raises(GameLaunchError) as ex_info:
await ladder_service.launch_game(game, host, [guest])
assert ex_info.value.players == [host]

# Host disconnected during game launch
host = player_factory("Dostya", player_id=1, lobby_connection_spec="auto")
disconnected_error = DisconnectedError("Test")
host.lobby_connection.launch_game.side_effect = disconnected_error

with pytest.raises(GameLaunchError) as ex_info:
await ladder_service.launch_game(game, host, [guest])
assert ex_info.value.players == [host]
assert ex_info.value.__cause__ == disconnected_error


async def test_launch_game_error_many_disconnected(
ladder_service: LadderService,
game,
player_factory
):
# Multiple players disconnect before game launch
host = player_factory("Dostya", player_id=1, lobby_connection_spec=None)
p2 = player_factory("Rhiza", player_id=2, lobby_connection_spec="auto")
p3 = player_factory("QAI", player_id=3, lobby_connection_spec=None)
p4 = player_factory("Brackman", player_id=4, lobby_connection_spec="auto")

with pytest.raises(GameLaunchError) as ex_info:
await ladder_service.launch_game(game, host, [p2, p3, p4])
assert ex_info.value.players == [host, p3]

# Players 3 and 4 disconnect during game launch
host = player_factory("Dostya", player_id=1, lobby_connection_spec="auto")
p3 = player_factory("QAI", player_id=3, lobby_connection_spec="auto")
disconnected_error = DisconnectedError("Test")
timeout_error = asyncio.TimeoutError()
p3.lobby_connection.launch_game.side_effect = disconnected_error
p4.lobby_connection.launch_game.side_effect = disconnected_error
game.wait_hosted = CoroutineMock()
game.wait_launched = CoroutineMock(side_effect=timeout_error)
game.state = GameState.LOBBY
add_connected_player(game, host)
add_connected_player(game, p2)

with pytest.raises(GameLaunchError) as ex_info:
await ladder_service.launch_game(game, host, [p2, p3, p4])
assert ex_info.value.players == [p3, p4]
assert ex_info.value.__cause__ == timeout_error


@given(
player1=st_players("p1", player_id=1, lobby_connection_spec="mock"),
player2=st_players("p2", player_id=2, lobby_connection_spec="mock"),
Expand Down

0 comments on commit 9de70f9

Please sign in to comment.