Skip to content

Commit

Permalink
Wait for all players to accept match before starting game
Browse files Browse the repository at this point in the history
  • Loading branch information
Askaholic committed Jul 25, 2020
1 parent 072fc36 commit c58c2e1
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 23 deletions.
70 changes: 53 additions & 17 deletions server/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import contextlib
import itertools
import re
import weakref
from collections import defaultdict
from typing import Dict, List, Optional, Set, Tuple
from datetime import datetime, timedelta
from typing import Dict, Iterable, List, Optional, Set, Tuple

import aiocron
from sqlalchemy import and_, func, select, text, true
Expand All @@ -30,7 +32,13 @@
from .decorators import with_logger
from .game_service import GameService
from .games import FeaturedModType, LadderGame
from .matchmaker import MapPool, MatchmakerQueue, Search
from .matchmaker import (
MapPool,
MatchmakerQueue,
MatchOffer,
OfferTimeoutError,
Search
)
from .players import Player, PlayerState
from .protocol import DisconnectedError
from .rating import RatingType
Expand Down Expand Up @@ -65,6 +73,7 @@ def __init__(
}

self.searches: Dict[str, Dict[Player, Search]] = defaultdict(dict)
self._match_offers = weakref.WeakValueDictionary()

async def initialize(self) -> None:
# TODO: Starting hardcoded queues here
Expand Down Expand Up @@ -319,21 +328,48 @@ def start_queue_handlers(self):

async def handle_queue_matches(self, queue: MatchmakerQueue):
async for s1, s2 in queue.iter_matches():
try:
msg = {"command": "match_found", "queue": queue.name}
# TODO: Handle disconnection with a client supported message
await asyncio.gather(*[
player.send_message(msg)
for player in s1.players + s2.players
])
asyncio.create_task(
self.start_game(s1.players, s2.players, queue)
)
except Exception as e:
self._logger.exception(
"Error processing match between searches %s, and %s: %s",
s1, s2, e
)
asyncio.create_task(self.handle_match(queue, s1, s2))

async def handle_match(
self,
queue: MatchmakerQueue,
s1: Search,
s2: Search
):
try:
msg = {"command": "match_found", "queue": queue.name}
all_players = s1.players + s2.players

for player in all_players:
player.write_message(msg)

offer = self.create_match_offer(all_players)
offer.write_broadcast_update()
await offer.wait_ready()

await self.start_game(s1.players, s2.players, queue)
except OfferTimeoutError:
self._logger.info(
"Match failed to start. Some players did not ready up in time: %s",
list(player.login for player in offer.get_unready_players())
)
# TODO: Unmatch and return to queue
except Exception as e:
self._logger.exception(
"Error processing match between searches %s, and %s: %s",
s1, s2, e
)

def create_match_offer(self, players: Iterable[Player]):
offer = MatchOffer(players, datetime.now() + timedelta(seconds=20))
for player in players:
self._match_offers[player] = offer
return offer

def ready_player(self, player: Player):
offer = self._match_offers.get(player)
if offer is not None:
offer.ready_player(player)

async def start_game(
self,
Expand Down
3 changes: 3 additions & 0 deletions server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,9 @@ async def command_game_host(self, message):
)
await self.launch_game(game, is_host=True)

async def command_match_ready(self, message):
self.ladder_service.ready_player(self.player)

async def launch_game(
self,
game,
Expand Down
6 changes: 5 additions & 1 deletion server/matchmaker/matchmaker_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ async def find_matches(self) -> None:
loop = asyncio.get_running_loop()
new_matches = filter(
lambda m: self.match(m[0], m[1]),
await loop.run_in_executor(None, make_matches, searches)
await loop.run_in_executor(
None,
make_matches,
(search for search in searches if not search.done())
)
)
self._matches.extend(new_matches)

Expand Down
9 changes: 6 additions & 3 deletions server/matchmaker/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ def quality_with(self, other: 'Search') -> float:
return quality([team1, team2])

@property
def is_matched(self):
def is_matched(self) -> bool:
return self._match.done() and not self._match.cancelled()

def done(self):
def done(self) -> bool:
return self._match.done()

@property
def is_cancelled(self):
def is_cancelled(self) -> bool:
return self._match.cancelled()

def matches_with(self, other: 'Search'):
Expand Down Expand Up @@ -213,6 +213,9 @@ def match(self, other: 'Search'):
))
self._match.set_result(other)

def unmatch(self):
self._match = asyncio.Future()

async def await_match(self):
"""
Wait for this search to complete
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ async def test_partial_game_ended_rates_game(lobby_server, tmp_user):
await read_until_command(host_proto, "player_info", timeout=10)


@fast_forward(15)
@fast_forward(70)
async def test_ladder_game_not_joinable(lobby_server):
"""
We should not be able to join AUTO_LOBBY games using the `game_join` command.
Expand Down
5 changes: 5 additions & 0 deletions tests/integration_tests/test_matchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ async def queue_players_for_matchmaking(lobby_server):
# If the players did not match, this will fail due to a timeout error
await read_until_command(proto1, 'match_found')
await read_until_command(proto2, 'match_found')
await read_until_command(proto1, 'match_info')
await read_until_command(proto2, 'match_info')

await proto1.send_message({"command": "match_ready"})
await proto2.send_message({"command": "match_ready"})

return proto1, proto2

Expand Down
12 changes: 11 additions & 1 deletion tests/integration_tests/test_teammatchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,17 @@ async def queue_players_for_matchmaking(lobby_server):


async def client_response(proto):
msg = await read_until_command(proto, "game_launch")
await read_until_command(proto, "match_info", timeout=5)
await proto.send_message({"command": "match_ready"})
await asyncio.wait_for(
read_until(
proto,
lambda msg:
msg["command"] == "match_info" and msg["players_ready"] == 4,
),
timeout=10,
)
msg = await read_until_command(proto, "game_launch", timeout=5)
# Ensures that the game enters the `LOBBY` state
await proto.send_message({
"command": "GameState",
Expand Down
5 changes: 5 additions & 0 deletions tests/unit_tests/test_ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,11 @@ async def test_start_game_called_on_match(ladder_service: LadderService, player_

await asyncio.sleep(2)

ladder_service.ready_player(p1)
ladder_service.ready_player(p2)

await asyncio.sleep(1)

ladder_service.inform_player.assert_called()
ladder_service.start_game.assert_called_once()

Expand Down

0 comments on commit c58c2e1

Please sign in to comment.