diff --git a/server/ladder_service/ladder_service.py b/server/ladder_service/ladder_service.py index 1f75d251d..89fc810d6 100644 --- a/server/ladder_service/ladder_service.py +++ b/server/ladder_service/ladder_service.py @@ -339,20 +339,27 @@ def start_search( on_matched=on_matched ) - for player in players: + self._add_search_to_queue(search, queue) + + def _add_search_to_queue( + self, + search: Search, + queue: MatchmakerQueue, + ): + for player in search.players: player.state = PlayerState.SEARCHING_LADDER self.write_rating_progress(player, queue.rating_type) player.write_message({ "command": "search_info", - "queue_name": queue_name, + "queue_name": queue.name, "state": "start" }) - self._searches[player][queue_name] = search + self._searches[player][queue.name] = search - self._logger.info("%s started searching for %s", search, queue_name) + self._logger.info("%s started searching for %s", search, queue.name) asyncio.create_task(queue.search(search)) @@ -418,26 +425,28 @@ def _clear_search( return search def write_rating_progress(self, player: Player, rating_type: str) -> None: - if player not in self._informed_players: - self._informed_players.add(player) - _, deviation = player.ratings[rating_type] + if player in self._informed_players: + return - if deviation > 490: - player.write_message({ - "command": "notice", - "style": "info", - "text": ( - "Welcome to the matchmaker

The " - "matchmaking system needs to calibrate your skill level; " - "your first few games may be more imbalanced as the " - "system attempts to learn your capability as a player." - "
" - "Afterwards, you'll be more reliably matched up with " - "people of your skill level: so don't worry if your " - "first few games are uneven. This will improve as you " - "play!" - ) - }) + self._informed_players.add(player) + _, deviation = player.ratings[rating_type] + + if deviation > 490: + player.write_message({ + "command": "notice", + "style": "info", + "text": ( + "Welcome to the matchmaker

The " + "matchmaking system needs to calibrate your skill level; " + "your first few games may be more imbalanced as the " + "system attempts to learn your capability as a player." + "
" + "Afterwards, you'll be more reliably matched up with " + "people of your skill level: so don't worry if your " + "first few games are uneven. This will improve as you " + "play!" + ) + }) def on_match_found( self, @@ -504,16 +513,29 @@ async def confirm_match( player.state = PlayerState.IDLE player.write_message(msg) - # Return any player that accepted the match back to the queue - # TODO: make this work with parties + # Return any search that fully accepted the match back to the queue for search in (s1, s2): - for player in search.players: - if player in unready_players: - self.cancel_search(player) - else: - search.unmatch() - player.state = PlayerState.SEARCHING_LADDER - asyncio.create_task(queue.search(search)) + search_players = search.players + search_unready_players = [ + player + for player in unready_players + if player in search_players + ] + if not search_unready_players: + search.unmatch() + self._add_search_to_queue(search, queue) + self._logger.debug( + "%s auto requeued after failed match", + search + ) + else: + for player in search_players: + player.write_message({ + "command": "match_notice", + "unready_players": [ + p.id for p in search_unready_players + ] + }) self.violation_service.register_violations(unready_players) diff --git a/tests/integration_tests/test_game.py b/tests/integration_tests/test_game.py index cbfe1cd2d..bc68a537e 100644 --- a/tests/integration_tests/test_game.py +++ b/tests/integration_tests/test_game.py @@ -219,13 +219,14 @@ async def queue_temp_players_for_matchmaking( tmp_user, num_players, queue_name, + player_name=None, ): """ Queue an arbitrary number of players for matchmaking in a particular queue by setting up temp users. """ users = await asyncio.gather(*[ - tmp_user(queue_name) + tmp_user(player_name or queue_name) for _ in range(num_players) ]) responses = await asyncio.gather(*[ diff --git a/tests/integration_tests/test_matchmaker.py b/tests/integration_tests/test_matchmaker.py index f9d2589ec..01f4cf3b6 100644 --- a/tests/integration_tests/test_matchmaker.py +++ b/tests/integration_tests/test_matchmaker.py @@ -418,6 +418,57 @@ async def test_game_matchmaking_close_fa_and_requeue(lobby_server): await read_until_command(proto1, "match_found", timeout=5) +@fast_forward(130) +async def test_game_matchmaking_no_accept_offer_auto_requeue( + lobby_server, + tmp_user, +): + proto1, proto2 = await queue_temp_players_for_matchmaking( + lobby_server, + tmp_user, + num_players=2, + queue_name="ladder1v1", + player_name="Player", + ) + + await read_until_command(proto1, "match_found", timeout=30) + await read_until_command(proto2, "match_found", timeout=5) + + # Only player 1 accepts the match + await read_until_command(proto1, "match_info", timeout=5) + await read_until_command(proto2, "match_info", timeout=5) + await proto1.send_message({"command": "match_ready"}) + await read_until_command(proto1, "match_cancelled", timeout=120) + + # Player 1 is automatically re-added to the queue, but player 2 is not + await read_until_command(proto1, "search_info", state="start", timeout=5) + msg = await read_until_command(proto1, "matchmaker_info", timeout=5) + queue_message = next( + queue + for queue in msg["queues"] + if queue["queue_name"] == "ladder1v1" + ) + assert queue_message["num_players"] == 1 + + # A third player joins the queue and is matched with player 1 + proto3, = await queue_temp_players_for_matchmaking( + lobby_server, + tmp_user, + num_players=1, + queue_name="ladder1v1", + player_name="Player", + ) + + await asyncio.gather(*[ + read_until_match(proto) + for proto in (proto1, proto3) + ]) + await asyncio.gather( + client_response(proto1), + client_response(proto3) + ) + + @pytest.mark.flaky @fast_forward(200) async def test_anti_map_repetition(lobby_server): @@ -567,7 +618,9 @@ async def read_update_msg(): msg = await read_until_command(proto, "matchmaker_info") queue_message = next( - q for q in msg["queues"] if q["queue_name"] == "ladder1v1" + queue + for queue in msg["queues"] + if queue["queue_name"] == "ladder1v1" ) if queue_message["num_players"] == 0: continue @@ -586,7 +639,11 @@ async def read_update_msg(): # Update message because we left the queue msg = await read_until_command(proto, "matchmaker_info") - queue_message = next(q for q in msg["queues"] if q["queue_name"] == "ladder1v1") + queue_message = next( + queue + for queue in msg["queues"] + if queue["queue_name"] == "ladder1v1" + ) assert queue_message["num_players"] == 0