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 Aug 29, 2021
1 parent af3e9ce commit 8859ba3
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 18 deletions.
5 changes: 4 additions & 1 deletion server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,17 @@ def __init__(self):
self.LADDER_ANTI_REPETITION_LIMIT = 2
self.LADDER_SEARCH_EXPANSION_MAX = 0.25
self.LADDER_SEARCH_EXPANSION_STEP = 0.05
# The maximum amount of time in seconds) to wait between pops.
# The maximum amount of time (in seconds) to wait between pops.
self.QUEUE_POP_TIME_MAX = 180
# The number of possible matches we would like to have when the queue
# pops. The queue pop time will be adjusted based on the current rate of
# players queuing to try and hit this number.
self.QUEUE_POP_DESIRED_MATCHES = 4
# How many previous queue sizes to consider
self.QUEUE_POP_TIME_MOVING_AVG_SIZE = 5
# Amount of time (in seconds) that players have to accept a match
# before it will time out.
self.MATCH_OFFER_TIME = 20

self._defaults = {
key: value for key, value in vars(self).items() if key.isupper()
Expand Down
59 changes: 54 additions & 5 deletions server/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import json
import random
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 @@ -33,7 +35,14 @@
from .decorators import with_logger
from .game_service import GameService
from .games import InitMode, LadderGame
from .matchmaker import MapPool, MatchmakerQueue, OnMatchedCallback, Search
from .matchmaker import (
MapPool,
MatchmakerQueue,
MatchOffer,
OfferTimeoutError,
OnMatchedCallback,
Search
)
from .players import Player, PlayerState
from .types import GameLaunchOptions, Map, NeroxisGeneratedMap

Expand All @@ -55,6 +64,7 @@ def __init__(
self.queues = {}

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

async def initialize(self) -> None:
await self.update_data()
Expand Down Expand Up @@ -347,15 +357,54 @@ def on_match_found(

self._clear_search(player, queue.name)

asyncio.create_task(
self.start_game(s1.players, s2.players, queue)
)
asyncio.create_task(self.confirm_match(s1, s2, queue))
except Exception:
self._logger.exception(
"Error processing match between searches %s, and %s",
s1, s2
)

async def confirm_match(
self,
s1: Search,
s2: Search,
queue: MatchmakerQueue
):
try:
all_players = s1.players + s2.players

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: Refactor duplication
msg = {"command": "match_cancelled"}
for player in all_players:
if player.state == PlayerState.STARTING_AUTOMATCH:
player.state = PlayerState.IDLE
player.write_message(msg)
# TODO: Unmatch and return to queue

def create_match_offer(self, players: Iterable[Player]):
offer = MatchOffer(
players,
datetime.now() + timedelta(seconds=config.MATCH_OFFER_TIME)
)
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,
team1: List[Player],
Expand Down
6 changes: 1 addition & 5 deletions server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,11 +996,7 @@ async def command_game_host(self, message):
await self.launch_game(game, is_host=True)

async def command_match_ready(self, message):
"""
Replace with full implementation when implemented in client, see:
https://github.com/FAForever/downlords-faf-client/issues/1783
"""
pass
self.ladder_service.ready_player(self.player)

async def launch_game(
self,
Expand Down
2 changes: 1 addition & 1 deletion server/matchmaker/matchmaker_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ async def find_matches(self) -> None:
proposed_matches, unmatched_searches = await loop.run_in_executor(
None,
self.matchmaker.find,
searches,
(search for search in searches if not search.done()),
self.team_size,
)

Expand Down
9 changes: 6 additions & 3 deletions server/matchmaker/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,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 @@ -219,6 +219,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
20 changes: 17 additions & 3 deletions tests/integration_tests/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ async def join_game(proto: Protocol, uid: int):


async def client_response(proto):
msg = await read_until_command(proto, "game_launch", timeout=10)
msg = await read_until_command(proto, "match_info", timeout=5)
await proto.send_message({"command": "match_ready"})
await read_until_command(
proto,
"match_info",
players_ready=msg["players_total"],
timeout=10
)
msg = await read_until_command(proto, "game_launch", timeout=5)

await open_fa(proto)
return msg

Expand Down Expand Up @@ -117,6 +126,11 @@ async def queue_players_for_matchmaking(lobby_server, queue_name: str = "ladder1
# If the players did not match, this will fail due to a timeout error
await read_until_command(proto1, "match_found", timeout=30)
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 Expand Up @@ -487,7 +501,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_draw_bug(lobby_server, database):
"""
This simulates the infamous "draw bug" where a player could self destruct
Expand Down Expand Up @@ -555,7 +569,7 @@ async def test_ladder_game_draw_bug(lobby_server, database):
assert row.score == 0


@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
14 changes: 14 additions & 0 deletions tests/integration_tests/test_teammatchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ async def queue_players_for_matchmaking(lobby_server):
read_until_command(proto, "match_found") for proto in protos
])

await asyncio.gather(*[
proto.send_message({
"command": "match_ready",
})
for proto in protos
])

return protos, ids


Expand Down Expand Up @@ -321,6 +328,13 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server):
)
assert msg["state"] == "stop"

await asyncio.gather(*[
proto.send_message({
"command": "match_ready",
})
for proto in protos
])

# Don't send any GPGNet messages so the match times out
await read_until_command(protos[0], "match_cancelled", timeout=120)

Expand Down
7 changes: 7 additions & 0 deletions tests/unit_tests/test_ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,13 @@ async def test_start_game_called_on_match(
search1 = ladder_service._searches[p1]["ladder1v1"]

await search1.await_match()
# Wait for offer to be created
await asyncio.sleep(1)

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

await asyncio.sleep(1)

ladder_service.write_rating_progress.assert_called()
ladder_service.start_game.assert_called_once()
Expand Down

0 comments on commit 8859ba3

Please sign in to comment.