647 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			647 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								# pylint: disable=W0212
							 | 
						||
| 
								 | 
							
								from __future__ import annotations
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import asyncio
							 | 
						||
| 
								 | 
							
								import json
							 | 
						||
| 
								 | 
							
								import platform
							 | 
						||
| 
								 | 
							
								import signal
							 | 
						||
| 
								 | 
							
								from contextlib import suppress
							 | 
						||
| 
								 | 
							
								from dataclasses import dataclass
							 | 
						||
| 
								 | 
							
								from io import BytesIO
							 | 
						||
| 
								 | 
							
								from pathlib import Path
							 | 
						||
| 
								 | 
							
								from typing import Dict, List, Optional, Tuple, Union
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import mpyq
							 | 
						||
| 
								 | 
							
								import portpicker
							 | 
						||
| 
								 | 
							
								from aiohttp import ClientSession, ClientWebSocketResponse
							 | 
						||
| 
								 | 
							
								from worlds._sc2common.bot import logger
							 | 
						||
| 
								 | 
							
								from s2clientprotocol import sc2api_pb2 as sc_pb
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from .bot_ai import BotAI
							 | 
						||
| 
								 | 
							
								from .client import Client
							 | 
						||
| 
								 | 
							
								from .controller import Controller
							 | 
						||
| 
								 | 
							
								from .data import CreateGameError, Result, Status
							 | 
						||
| 
								 | 
							
								from .game_state import GameState
							 | 
						||
| 
								 | 
							
								from .maps import Map
							 | 
						||
| 
								 | 
							
								from .player import AbstractPlayer, Bot, BotProcess, Human
							 | 
						||
| 
								 | 
							
								from .portconfig import Portconfig
							 | 
						||
| 
								 | 
							
								from .protocol import ConnectionAlreadyClosed, ProtocolError
							 | 
						||
| 
								 | 
							
								from .proxy import Proxy
							 | 
						||
| 
								 | 
							
								from .sc2process import SC2Process, kill_switch
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@dataclass
							 | 
						||
| 
								 | 
							
								class GameMatch:
							 | 
						||
| 
								 | 
							
								    """Dataclass for hosting a match of SC2.
							 | 
						||
| 
								 | 
							
								    This contains all of the needed information for RequestCreateGame.
							 | 
						||
| 
								 | 
							
								    :param sc2_config: dicts of arguments to unpack into sc2process's construction, one per player
							 | 
						||
| 
								 | 
							
								        second sc2_config will be ignored if only one sc2_instance is spawned
							 | 
						||
| 
								 | 
							
								        e.g. sc2_args=[{"fullscreen": True}, {}]: only player 1's sc2instance will be fullscreen
							 | 
						||
| 
								 | 
							
								    :param game_time_limit: The time (in seconds) until a match is artificially declared a Tie
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    map_sc2: Map
							 | 
						||
| 
								 | 
							
								    players: List[AbstractPlayer]
							 | 
						||
| 
								 | 
							
								    realtime: bool = False
							 | 
						||
| 
								 | 
							
								    random_seed: int = None
							 | 
						||
| 
								 | 
							
								    disable_fog: bool = None
							 | 
						||
| 
								 | 
							
								    sc2_config: List[Dict] = None
							 | 
						||
| 
								 | 
							
								    game_time_limit: int = None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __post_init__(self):
							 | 
						||
| 
								 | 
							
								        # avoid players sharing names
							 | 
						||
| 
								 | 
							
								        if len(self.players) > 1 and self.players[0].name is not None and self.players[0].name == self.players[1].name:
							 | 
						||
| 
								 | 
							
								            self.players[1].name += "2"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.sc2_config is not None:
							 | 
						||
| 
								 | 
							
								            if isinstance(self.sc2_config, dict):
							 | 
						||
| 
								 | 
							
								                self.sc2_config = [self.sc2_config]
							 | 
						||
| 
								 | 
							
								            if len(self.sc2_config) == 0:
							 | 
						||
| 
								 | 
							
								                self.sc2_config = [{}]
							 | 
						||
| 
								 | 
							
								            while len(self.sc2_config) < len(self.players):
							 | 
						||
| 
								 | 
							
								                self.sc2_config += self.sc2_config
							 | 
						||
| 
								 | 
							
								            self.sc2_config = self.sc2_config[:len(self.players)]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def needed_sc2_count(self) -> int:
							 | 
						||
| 
								 | 
							
								        return sum(player.needs_sc2 for player in self.players)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @property
							 | 
						||
| 
								 | 
							
								    def host_game_kwargs(self) -> Dict:
							 | 
						||
| 
								 | 
							
								        return {
							 | 
						||
| 
								 | 
							
								            "map_settings": self.map_sc2,
							 | 
						||
| 
								 | 
							
								            "players": self.players,
							 | 
						||
| 
								 | 
							
								            "realtime": self.realtime,
							 | 
						||
| 
								 | 
							
								            "random_seed": self.random_seed,
							 | 
						||
| 
								 | 
							
								            "disable_fog": self.disable_fog,
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __repr__(self):
							 | 
						||
| 
								 | 
							
								        p1 = self.players[0]
							 | 
						||
| 
								 | 
							
								        p1 = p1.name if p1.name else p1
							 | 
						||
| 
								 | 
							
								        p2 = self.players[1]
							 | 
						||
| 
								 | 
							
								        p2 = p2.name if p2.name else p2
							 | 
						||
| 
								 | 
							
								        return f"Map: {self.map_sc2.name}, {p1} vs {p2}, realtime={self.realtime}, seed={self.random_seed}"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _play_game_human(client, player_id, realtime, game_time_limit):
							 | 
						||
| 
								 | 
							
								    while True:
							 | 
						||
| 
								 | 
							
								        state = await client.observation()
							 | 
						||
| 
								 | 
							
								        if client._game_result:
							 | 
						||
| 
								 | 
							
								            return client._game_result[player_id]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if game_time_limit and state.observation.observation.game_loop / 22.4 > game_time_limit:
							 | 
						||
| 
								 | 
							
								            logger.info(state.observation.game_loop, state.observation.game_loop / 22.4)
							 | 
						||
| 
								 | 
							
								            return Result.Tie
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if not realtime:
							 | 
						||
| 
								 | 
							
								            await client.step()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# pylint: disable=R0912,R0911,R0914
							 | 
						||
| 
								 | 
							
								async def _play_game_ai(
							 | 
						||
| 
								 | 
							
								    client: Client, player_id: int, ai: BotAI, realtime: bool, game_time_limit: Optional[int]
							 | 
						||
| 
								 | 
							
								) -> Result:
							 | 
						||
| 
								 | 
							
								    gs: GameState = None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    async def initialize_first_step() -> Optional[Result]:
							 | 
						||
| 
								 | 
							
								        nonlocal gs
							 | 
						||
| 
								 | 
							
								        ai._initialize_variables()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        game_data = await client.get_game_data()
							 | 
						||
| 
								 | 
							
								        game_info = await client.get_game_info()
							 | 
						||
| 
								 | 
							
								        ping_response = await client.ping()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # This game_data will become self.game_data in botAI
							 | 
						||
| 
								 | 
							
								        ai._prepare_start(
							 | 
						||
| 
								 | 
							
								            client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								        state = await client.observation()
							 | 
						||
| 
								 | 
							
								        # check game result every time we get the observation
							 | 
						||
| 
								 | 
							
								        if client._game_result:
							 | 
						||
| 
								 | 
							
								            await ai.on_end(client._game_result[player_id])
							 | 
						||
| 
								 | 
							
								            return client._game_result[player_id]
							 | 
						||
| 
								 | 
							
								        gs = GameState(state.observation)
							 | 
						||
| 
								 | 
							
								        proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            ai._prepare_step(gs, proto_game_info)
							 | 
						||
| 
								 | 
							
								            await ai.on_before_start()
							 | 
						||
| 
								 | 
							
								            ai._prepare_first_step()
							 | 
						||
| 
								 | 
							
								            await ai.on_start()
							 | 
						||
| 
								 | 
							
								        # TODO Catching too general exception Exception (broad-except)
							 | 
						||
| 
								 | 
							
								        # pylint: disable=W0703
							 | 
						||
| 
								 | 
							
								        except Exception as e:
							 | 
						||
| 
								 | 
							
								            logger.exception(f"Caught unknown exception in AI on_start: {e}")
							 | 
						||
| 
								 | 
							
								            logger.error("Resigning due to previous error")
							 | 
						||
| 
								 | 
							
								            await ai.on_end(Result.Defeat)
							 | 
						||
| 
								 | 
							
								            return Result.Defeat
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    result = await initialize_first_step()
							 | 
						||
| 
								 | 
							
								    if result is not None:
							 | 
						||
| 
								 | 
							
								        return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    async def run_bot_iteration(iteration: int):
							 | 
						||
| 
								 | 
							
								        nonlocal gs
							 | 
						||
| 
								 | 
							
								        logger.debug(f"Running AI step, it={iteration} {gs.game_loop / 22.4:.2f}s")
							 | 
						||
| 
								 | 
							
								        # Issue event like unit created or unit destroyed
							 | 
						||
| 
								 | 
							
								        await ai.issue_events()
							 | 
						||
| 
								 | 
							
								        # In on_step various errors can occur - log properly
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            await ai.on_step(iteration)
							 | 
						||
| 
								 | 
							
								        except (AttributeError, ) as e:
							 | 
						||
| 
								 | 
							
								            logger.exception(f"Caught exception: {e}")
							 | 
						||
| 
								 | 
							
								            raise
							 | 
						||
| 
								 | 
							
								        except Exception as e:
							 | 
						||
| 
								 | 
							
								            logger.exception(f"Caught unknown exception: {e}")
							 | 
						||
| 
								 | 
							
								            raise
							 | 
						||
| 
								 | 
							
								        await ai._after_step()
							 | 
						||
| 
								 | 
							
								        logger.debug("Running AI step: done")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Only used in realtime=True
							 | 
						||
| 
								 | 
							
								    previous_state_observation = None
							 | 
						||
| 
								 | 
							
								    for iteration in range(10**10):
							 | 
						||
| 
								 | 
							
								        if realtime and gs:
							 | 
						||
| 
								 | 
							
								            # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game']
							 | 
						||
| 
								 | 
							
								            with suppress(ProtocolError):
							 | 
						||
| 
								 | 
							
								                requested_step = gs.game_loop + client.game_step
							 | 
						||
| 
								 | 
							
								                state = await client.observation(requested_step)
							 | 
						||
| 
								 | 
							
								                # If the bot took too long in the previous observation, request another observation one frame after
							 | 
						||
| 
								 | 
							
								                if state.observation.observation.game_loop > requested_step:
							 | 
						||
| 
								 | 
							
								                    logger.debug("Skipped a step in realtime=True")
							 | 
						||
| 
								 | 
							
								                    previous_state_observation = state.observation
							 | 
						||
| 
								 | 
							
								                    state = await client.observation(state.observation.observation.game_loop + 1)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            state = await client.observation()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # check game result every time we get the observation
							 | 
						||
| 
								 | 
							
								        if client._game_result:
							 | 
						||
| 
								 | 
							
								            await ai.on_end(client._game_result[player_id])
							 | 
						||
| 
								 | 
							
								            return client._game_result[player_id]
							 | 
						||
| 
								 | 
							
								        gs = GameState(state.observation, previous_state_observation)
							 | 
						||
| 
								 | 
							
								        previous_state_observation = None
							 | 
						||
| 
								 | 
							
								        logger.debug(f"Score: {gs.score.score}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if game_time_limit and gs.game_loop / 22.4 > game_time_limit:
							 | 
						||
| 
								 | 
							
								            await ai.on_end(Result.Tie)
							 | 
						||
| 
								 | 
							
								            return Result.Tie
							 | 
						||
| 
								 | 
							
								        proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())
							 | 
						||
| 
								 | 
							
								        ai._prepare_step(gs, proto_game_info)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        await run_bot_iteration(iteration)  # Main bot loop
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if not realtime:
							 | 
						||
| 
								 | 
							
								            if not client.in_game:  # Client left (resigned) the game
							 | 
						||
| 
								 | 
							
								                await ai.on_end(client._game_result[player_id])
							 | 
						||
| 
								 | 
							
								                return client._game_result[player_id]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # TODO: In bot vs bot, if the other bot ends the game, this bot gets stuck in requesting an observation when using main.py:run_multiple_games
							 | 
						||
| 
								 | 
							
								            await client.step()
							 | 
						||
| 
								 | 
							
								    return Result.Undecided
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _play_game(
							 | 
						||
| 
								 | 
							
								    player: AbstractPlayer,
							 | 
						||
| 
								 | 
							
								    client: Client,
							 | 
						||
| 
								 | 
							
								    realtime,
							 | 
						||
| 
								 | 
							
								    portconfig,
							 | 
						||
| 
								 | 
							
								    game_time_limit=None,
							 | 
						||
| 
								 | 
							
								    rgb_render_config=None
							 | 
						||
| 
								 | 
							
								) -> Result:
							 | 
						||
| 
								 | 
							
								    assert isinstance(realtime, bool), repr(realtime)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    player_id = await client.join_game(
							 | 
						||
| 
								 | 
							
								        player.name, player.race, portconfig=portconfig, rgb_render_config=rgb_render_config
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								    logger.info(f"Player {player_id} - {player.name if player.name else str(player)}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if isinstance(player, Human):
							 | 
						||
| 
								 | 
							
								        result = await _play_game_human(client, player_id, realtime, game_time_limit)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        result = await _play_game_ai(client, player_id, player.ai, realtime, game_time_limit)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    logger.info(
							 | 
						||
| 
								 | 
							
								        f"Result for player {player_id} - {player.name if player.name else str(player)}: "
							 | 
						||
| 
								 | 
							
								        f"{result._name_ if isinstance(result, Result) else result}"
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _setup_host_game(
							 | 
						||
| 
								 | 
							
								    server: Controller, map_settings, players, realtime, random_seed=None, disable_fog=None, save_replay_as=None
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								    r = await server.create_game(map_settings, players, realtime, random_seed, disable_fog)
							 | 
						||
| 
								 | 
							
								    if r.create_game.HasField("error"):
							 | 
						||
| 
								 | 
							
								        err = f"Could not create game: {CreateGameError(r.create_game.error)}"
							 | 
						||
| 
								 | 
							
								        if r.create_game.HasField("error_details"):
							 | 
						||
| 
								 | 
							
								            err += f": {r.create_game.error_details}"
							 | 
						||
| 
								 | 
							
								        logger.critical(err)
							 | 
						||
| 
								 | 
							
								        raise RuntimeError(err)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return Client(server._ws, save_replay_as)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _host_game(
							 | 
						||
| 
								 | 
							
								    map_settings,
							 | 
						||
| 
								 | 
							
								    players,
							 | 
						||
| 
								 | 
							
								    realtime=False,
							 | 
						||
| 
								 | 
							
								    portconfig=None,
							 | 
						||
| 
								 | 
							
								    save_replay_as=None,
							 | 
						||
| 
								 | 
							
								    game_time_limit=None,
							 | 
						||
| 
								 | 
							
								    rgb_render_config=None,
							 | 
						||
| 
								 | 
							
								    random_seed=None,
							 | 
						||
| 
								 | 
							
								    sc2_version=None,
							 | 
						||
| 
								 | 
							
								    disable_fog=None,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    assert players, "Can't create a game without players"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    assert any(isinstance(p, (Human, Bot)) for p in players)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    async with SC2Process(
							 | 
						||
| 
								 | 
							
								        fullscreen=players[0].fullscreen, render=rgb_render_config is not None, sc2_version=sc2_version
							 | 
						||
| 
								 | 
							
								    ) as server:
							 | 
						||
| 
								 | 
							
								        await server.ping()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        client = await _setup_host_game(
							 | 
						||
| 
								 | 
							
								            server, map_settings, players, realtime, random_seed, disable_fog, save_replay_as
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								        # Bot can decide if it wants to launch with 'raw_affects_selection=True'
							 | 
						||
| 
								 | 
							
								        if not isinstance(players[0], Human) and getattr(players[0].ai, "raw_affects_selection", None) is not None:
							 | 
						||
| 
								 | 
							
								            client.raw_affects_selection = players[0].ai.raw_affects_selection
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        result = await _play_game(players[0], client, realtime, portconfig, game_time_limit, rgb_render_config)
							 | 
						||
| 
								 | 
							
								        if client.save_replay_path is not None:
							 | 
						||
| 
								 | 
							
								            await client.save_replay(client.save_replay_path)
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            await client.leave()
							 | 
						||
| 
								 | 
							
								        except ConnectionAlreadyClosed:
							 | 
						||
| 
								 | 
							
								            logger.error("Connection was closed before the game ended")
							 | 
						||
| 
								 | 
							
								        await client.quit()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _host_game_aiter(
							 | 
						||
| 
								 | 
							
								    map_settings,
							 | 
						||
| 
								 | 
							
								    players,
							 | 
						||
| 
								 | 
							
								    realtime,
							 | 
						||
| 
								 | 
							
								    portconfig=None,
							 | 
						||
| 
								 | 
							
								    save_replay_as=None,
							 | 
						||
| 
								 | 
							
								    game_time_limit=None,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								    assert players, "Can't create a game without players"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    assert any(isinstance(p, (Human, Bot)) for p in players)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    async with SC2Process() as server:
							 | 
						||
| 
								 | 
							
								        while True:
							 | 
						||
| 
								 | 
							
								            await server.ping()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            client = await _setup_host_game(server, map_settings, players, realtime)
							 | 
						||
| 
								 | 
							
								            if not isinstance(players[0], Human) and getattr(players[0].ai, "raw_affects_selection", None) is not None:
							 | 
						||
| 
								 | 
							
								                client.raw_affects_selection = players[0].ai.raw_affects_selection
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                result = await _play_game(players[0], client, realtime, portconfig, game_time_limit)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                if save_replay_as is not None:
							 | 
						||
| 
								 | 
							
								                    await client.save_replay(save_replay_as)
							 | 
						||
| 
								 | 
							
								                await client.leave()
							 | 
						||
| 
								 | 
							
								            except ConnectionAlreadyClosed:
							 | 
						||
| 
								 | 
							
								                logger.error("Connection was closed before the game ended")
							 | 
						||
| 
								 | 
							
								                return
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            new_players = yield result
							 | 
						||
| 
								 | 
							
								            if new_players is not None:
							 | 
						||
| 
								 | 
							
								                players = new_players
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def _host_game_iter(*args, **kwargs):
							 | 
						||
| 
								 | 
							
								    game = _host_game_aiter(*args, **kwargs)
							 | 
						||
| 
								 | 
							
								    new_playerconfig = None
							 | 
						||
| 
								 | 
							
								    while True:
							 | 
						||
| 
								 | 
							
								        new_playerconfig = yield asyncio.get_event_loop().run_until_complete(game.asend(new_playerconfig))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def _join_game(
							 | 
						||
| 
								 | 
							
								    players,
							 | 
						||
| 
								 | 
							
								    realtime,
							 | 
						||
| 
								 | 
							
								    portconfig,
							 | 
						||
| 
								 | 
							
								    save_replay_as=None,
							 | 
						||
| 
								 | 
							
								    game_time_limit=None,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								    async with SC2Process(fullscreen=players[1].fullscreen) as server:
							 | 
						||
| 
								 | 
							
								        await server.ping()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        client = Client(server._ws)
							 | 
						||
| 
								 | 
							
								        # Bot can decide if it wants to launch with 'raw_affects_selection=True'
							 | 
						||
| 
								 | 
							
								        if not isinstance(players[1], Human) and getattr(players[1].ai, "raw_affects_selection", None) is not None:
							 | 
						||
| 
								 | 
							
								            client.raw_affects_selection = players[1].ai.raw_affects_selection
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        result = await _play_game(players[1], client, realtime, portconfig, game_time_limit)
							 | 
						||
| 
								 | 
							
								        if save_replay_as is not None:
							 | 
						||
| 
								 | 
							
								            await client.save_replay(save_replay_as)
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            await client.leave()
							 | 
						||
| 
								 | 
							
								        except ConnectionAlreadyClosed:
							 | 
						||
| 
								 | 
							
								            logger.error("Connection was closed before the game ended")
							 | 
						||
| 
								 | 
							
								        await client.quit()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_replay_version(replay_path: Union[str, Path]) -> Tuple[str, str]:
							 | 
						||
| 
								 | 
							
								    with open(replay_path, 'rb') as f:
							 | 
						||
| 
								 | 
							
								        replay_data = f.read()
							 | 
						||
| 
								 | 
							
								        replay_io = BytesIO()
							 | 
						||
| 
								 | 
							
								        replay_io.write(replay_data)
							 | 
						||
| 
								 | 
							
								        replay_io.seek(0)
							 | 
						||
| 
								 | 
							
								        archive = mpyq.MPQArchive(replay_io).extract()
							 | 
						||
| 
								 | 
							
								        metadata = json.loads(archive[b"replay.gamemetadata.json"].decode("utf-8"))
							 | 
						||
| 
								 | 
							
								        return metadata["BaseBuild"], metadata["DataVersion"]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# TODO Deprecate run_game function in favor of run_multiple_games
							 | 
						||
| 
								 | 
							
								def run_game(map_settings, players, **kwargs) -> Union[Result, List[Optional[Result]]]:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    Returns a single Result enum if the game was against the built-in computer.
							 | 
						||
| 
								 | 
							
								    Returns a list of two Result enums if the game was "Human vs Bot" or "Bot vs Bot".
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    if sum(isinstance(p, (Human, Bot)) for p in players) > 1:
							 | 
						||
| 
								 | 
							
								        host_only_args = ["save_replay_as", "rgb_render_config", "random_seed", "sc2_version", "disable_fog"]
							 | 
						||
| 
								 | 
							
								        join_kwargs = {k: v for k, v in kwargs.items() if k not in host_only_args}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        portconfig = Portconfig()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        async def run_host_and_join():
							 | 
						||
| 
								 | 
							
								            return await asyncio.gather(
							 | 
						||
| 
								 | 
							
								                _host_game(map_settings, players, **kwargs, portconfig=portconfig),
							 | 
						||
| 
								 | 
							
								                _join_game(players, **join_kwargs, portconfig=portconfig),
							 | 
						||
| 
								 | 
							
								                return_exceptions=True
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        result: List[Result] = asyncio.run(run_host_and_join())
							 | 
						||
| 
								 | 
							
								        assert isinstance(result, list)
							 | 
						||
| 
								 | 
							
								        assert all(isinstance(r, Result) for r in result)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        result: Result = asyncio.run(_host_game(map_settings, players, **kwargs))
							 | 
						||
| 
								 | 
							
								        assert isinstance(result, Result)
							 | 
						||
| 
								 | 
							
								    return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def play_from_websocket(
							 | 
						||
| 
								 | 
							
								    ws_connection: Union[str, ClientWebSocketResponse],
							 | 
						||
| 
								 | 
							
								    player: AbstractPlayer,
							 | 
						||
| 
								 | 
							
								    realtime: bool = False,
							 | 
						||
| 
								 | 
							
								    portconfig: Portconfig = None,
							 | 
						||
| 
								 | 
							
								    save_replay_as=None,
							 | 
						||
| 
								 | 
							
								    game_time_limit: int = None,
							 | 
						||
| 
								 | 
							
								    should_close=True,
							 | 
						||
| 
								 | 
							
								):
							 | 
						||
| 
								 | 
							
								    """Use this to play when the match is handled externally e.g. for bot ladder games.
							 | 
						||
| 
								 | 
							
								    Portconfig MUST be specified if not playing vs Computer.
							 | 
						||
| 
								 | 
							
								    :param ws_connection: either a string("ws://{address}:{port}/sc2api") or a ClientWebSocketResponse object
							 | 
						||
| 
								 | 
							
								    :param should_close: closes the connection if True. Use False if something else will reuse the connection
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    e.g. ladder usage: play_from_websocket("ws://127.0.0.1:5162/sc2api", MyBot, False, portconfig=my_PC)
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    session = None
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        if isinstance(ws_connection, str):
							 | 
						||
| 
								 | 
							
								            session = ClientSession()
							 | 
						||
| 
								 | 
							
								            ws_connection = await session.ws_connect(ws_connection, timeout=120)
							 | 
						||
| 
								 | 
							
								            should_close = True
							 | 
						||
| 
								 | 
							
								        client = Client(ws_connection)
							 | 
						||
| 
								 | 
							
								        result = await _play_game(player, client, realtime, portconfig, game_time_limit=game_time_limit)
							 | 
						||
| 
								 | 
							
								        if save_replay_as is not None:
							 | 
						||
| 
								 | 
							
								            await client.save_replay(save_replay_as)
							 | 
						||
| 
								 | 
							
								    except ConnectionAlreadyClosed:
							 | 
						||
| 
								 | 
							
								        logger.error("Connection was closed before the game ended")
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								    finally:
							 | 
						||
| 
								 | 
							
								        if should_close:
							 | 
						||
| 
								 | 
							
								            await ws_connection.close()
							 | 
						||
| 
								 | 
							
								            if session:
							 | 
						||
| 
								 | 
							
								                await session.close()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async def run_match(controllers: List[Controller], match: GameMatch, close_ws=True):
							 | 
						||
| 
								 | 
							
								    await _setup_host_game(controllers[0], **match.host_game_kwargs)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Setup portconfig beforehand, so all players use the same ports
							 | 
						||
| 
								 | 
							
								    startport = None
							 | 
						||
| 
								 | 
							
								    portconfig = None
							 | 
						||
| 
								 | 
							
								    if match.needed_sc2_count > 1:
							 | 
						||
| 
								 | 
							
								        if any(isinstance(player, BotProcess) for player in match.players):
							 | 
						||
| 
								 | 
							
								            portconfig = Portconfig.contiguous_ports()
							 | 
						||
| 
								 | 
							
								            # Most ladder bots generate their server and client ports as [s+2, s+3], [s+4, s+5]
							 | 
						||
| 
								 | 
							
								            startport = portconfig.server[0] - 2
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            portconfig = Portconfig()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    proxies = []
							 | 
						||
| 
								 | 
							
								    coros = []
							 | 
						||
| 
								 | 
							
								    players_that_need_sc2 = filter(lambda lambda_player: lambda_player.needs_sc2, match.players)
							 | 
						||
| 
								 | 
							
								    for i, player in enumerate(players_that_need_sc2):
							 | 
						||
| 
								 | 
							
								        if isinstance(player, BotProcess):
							 | 
						||
| 
								 | 
							
								            pport = portpicker.pick_unused_port()
							 | 
						||
| 
								 | 
							
								            p = Proxy(controllers[i], player, pport, match.game_time_limit, match.realtime)
							 | 
						||
| 
								 | 
							
								            proxies.append(p)
							 | 
						||
| 
								 | 
							
								            coros.append(p.play_with_proxy(startport))
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            coros.append(
							 | 
						||
| 
								 | 
							
								                play_from_websocket(
							 | 
						||
| 
								 | 
							
								                    controllers[i]._ws,
							 | 
						||
| 
								 | 
							
								                    player,
							 | 
						||
| 
								 | 
							
								                    match.realtime,
							 | 
						||
| 
								 | 
							
								                    portconfig,
							 | 
						||
| 
								 | 
							
								                    should_close=close_ws,
							 | 
						||
| 
								 | 
							
								                    game_time_limit=match.game_time_limit,
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    async_results = await asyncio.gather(*coros, return_exceptions=True)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not isinstance(async_results, list):
							 | 
						||
| 
								 | 
							
								        async_results = [async_results]
							 | 
						||
| 
								 | 
							
								    for i, a in enumerate(async_results):
							 | 
						||
| 
								 | 
							
								        if isinstance(a, Exception):
							 | 
						||
| 
								 | 
							
								            logger.error(f"Exception[{a}] thrown by {[p for p in match.players if p.needs_sc2][i]}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return process_results(match.players, async_results)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def process_results(players: List[AbstractPlayer], async_results: List[Result]) -> Dict[AbstractPlayer, Result]:
							 | 
						||
| 
								 | 
							
								    opp_res = {Result.Victory: Result.Defeat, Result.Defeat: Result.Victory, Result.Tie: Result.Tie}
							 | 
						||
| 
								 | 
							
								    result: Dict[AbstractPlayer, Result] = {}
							 | 
						||
| 
								 | 
							
								    i = 0
							 | 
						||
| 
								 | 
							
								    for player in players:
							 | 
						||
| 
								 | 
							
								        if player.needs_sc2:
							 | 
						||
| 
								 | 
							
								            if sum(r == Result.Victory for r in async_results) <= 1:
							 | 
						||
| 
								 | 
							
								                result[player] = async_results[i]
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                result[player] = Result.Undecided
							 | 
						||
| 
								 | 
							
								            i += 1
							 | 
						||
| 
								 | 
							
								        else:  # computer
							 | 
						||
| 
								 | 
							
								            other_result = async_results[0]
							 | 
						||
| 
								 | 
							
								            result[player] = None
							 | 
						||
| 
								 | 
							
								            if other_result in opp_res:
							 | 
						||
| 
								 | 
							
								                result[player] = opp_res[other_result]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return result
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# pylint: disable=R0912
							 | 
						||
| 
								 | 
							
								async def maintain_SCII_count(count: int, controllers: List[Controller], proc_args: List[Dict] = None):
							 | 
						||
| 
								 | 
							
								    """Modifies the given list of controllers to reflect the desired amount of SCII processes"""
							 | 
						||
| 
								 | 
							
								    # kill unhealthy ones.
							 | 
						||
| 
								 | 
							
								    if controllers:
							 | 
						||
| 
								 | 
							
								        to_remove = []
							 | 
						||
| 
								 | 
							
								        alive = await asyncio.wait_for(
							 | 
						||
| 
								 | 
							
								            asyncio.gather(*(c.ping() for c in controllers if not c._ws.closed), return_exceptions=True), timeout=20
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								        i = 0  # for alive
							 | 
						||
| 
								 | 
							
								        for controller in controllers:
							 | 
						||
| 
								 | 
							
								            if controller._ws.closed:
							 | 
						||
| 
								 | 
							
								                if not controller._process._session.closed:
							 | 
						||
| 
								 | 
							
								                    await controller._process._session.close()
							 | 
						||
| 
								 | 
							
								                to_remove.append(controller)
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                if not isinstance(alive[i], sc_pb.Response):
							 | 
						||
| 
								 | 
							
								                    try:
							 | 
						||
| 
								 | 
							
								                        await controller._process._close_connection()
							 | 
						||
| 
								 | 
							
								                    finally:
							 | 
						||
| 
								 | 
							
								                        to_remove.append(controller)
							 | 
						||
| 
								 | 
							
								                i += 1
							 | 
						||
| 
								 | 
							
								        for c in to_remove:
							 | 
						||
| 
								 | 
							
								            c._process._clean(verbose=False)
							 | 
						||
| 
								 | 
							
								            if c._process in kill_switch._to_kill:
							 | 
						||
| 
								 | 
							
								                kill_switch._to_kill.remove(c._process)
							 | 
						||
| 
								 | 
							
								            controllers.remove(c)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # spawn more
							 | 
						||
| 
								 | 
							
								    if len(controllers) < count:
							 | 
						||
| 
								 | 
							
								        needed = count - len(controllers)
							 | 
						||
| 
								 | 
							
								        if proc_args:
							 | 
						||
| 
								 | 
							
								            index = len(controllers) % len(proc_args)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            proc_args = [{} for _ in range(needed)]
							 | 
						||
| 
								 | 
							
								            index = 0
							 | 
						||
| 
								 | 
							
								        extra = [SC2Process(**proc_args[(index + _) % len(proc_args)]) for _ in range(needed)]
							 | 
						||
| 
								 | 
							
								        logger.info(f"Creating {needed} more SC2 Processes")
							 | 
						||
| 
								 | 
							
								        for _ in range(3):
							 | 
						||
| 
								 | 
							
								            if platform.system() == "Linux":
							 | 
						||
| 
								 | 
							
								                # Works on linux: start one client after the other
							 | 
						||
| 
								 | 
							
								                # pylint: disable=C2801
							 | 
						||
| 
								 | 
							
								                new_controllers = [await asyncio.wait_for(sc.__aenter__(), timeout=50) for sc in extra]
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                # Doesnt seem to work on linux: starting 2 clients nearly at the same time
							 | 
						||
| 
								 | 
							
								                new_controllers = await asyncio.wait_for(
							 | 
						||
| 
								 | 
							
								                    # pylint: disable=C2801
							 | 
						||
| 
								 | 
							
								                    asyncio.gather(*[sc.__aenter__() for sc in extra], return_exceptions=True),
							 | 
						||
| 
								 | 
							
								                    timeout=50
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            controllers.extend(c for c in new_controllers if isinstance(c, Controller))
							 | 
						||
| 
								 | 
							
								            if len(controllers) == count:
							 | 
						||
| 
								 | 
							
								                await asyncio.wait_for(asyncio.gather(*(c.ping() for c in controllers)), timeout=20)
							 | 
						||
| 
								 | 
							
								                break
							 | 
						||
| 
								 | 
							
								            extra = [
							 | 
						||
| 
								 | 
							
								                extra[i] for i, result in enumerate(new_controllers) if not isinstance(new_controllers, Controller)
							 | 
						||
| 
								 | 
							
								            ]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            logger.critical("Could not launch sufficient SC2")
							 | 
						||
| 
								 | 
							
								            raise RuntimeError
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # kill excess
							 | 
						||
| 
								 | 
							
								    while len(controllers) > count:
							 | 
						||
| 
								 | 
							
								        proc = controllers.pop()
							 | 
						||
| 
								 | 
							
								        proc = proc._process
							 | 
						||
| 
								 | 
							
								        logger.info(f"Removing SCII listening to {proc._port}")
							 | 
						||
| 
								 | 
							
								        await proc._close_connection()
							 | 
						||
| 
								 | 
							
								        proc._clean(verbose=False)
							 | 
						||
| 
								 | 
							
								        if proc in kill_switch._to_kill:
							 | 
						||
| 
								 | 
							
								            kill_switch._to_kill.remove(proc)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def run_multiple_games(matches: List[GameMatch]):
							 | 
						||
| 
								 | 
							
								    return asyncio.get_event_loop().run_until_complete(a_run_multiple_games(matches))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# TODO Catching too general exception Exception (broad-except)
							 | 
						||
| 
								 | 
							
								# pylint: disable=W0703
							 | 
						||
| 
								 | 
							
								async def a_run_multiple_games(matches: List[GameMatch]) -> List[Dict[AbstractPlayer, Result]]:
							 | 
						||
| 
								 | 
							
								    """Run multiple matches.
							 | 
						||
| 
								 | 
							
								    Non-python bots are supported.
							 | 
						||
| 
								 | 
							
								    When playing bot vs bot, this is less likely to fatally crash than repeating run_game()
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    if not matches:
							 | 
						||
| 
								 | 
							
								        return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    results = []
							 | 
						||
| 
								 | 
							
								    controllers = []
							 | 
						||
| 
								 | 
							
								    for m in matches:
							 | 
						||
| 
								 | 
							
								        result = None
							 | 
						||
| 
								 | 
							
								        dont_restart = m.needed_sc2_count == 2
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config)
							 | 
						||
| 
								 | 
							
								            result = await run_match(controllers, m, close_ws=dont_restart)
							 | 
						||
| 
								 | 
							
								        except SystemExit as e:
							 | 
						||
| 
								 | 
							
								            logger.info(f"Game exit'ed as {e} during match {m}")
							 | 
						||
| 
								 | 
							
								        except Exception as e:
							 | 
						||
| 
								 | 
							
								            logger.exception(f"Caught unknown exception: {e}")
							 | 
						||
| 
								 | 
							
								            logger.info(f"Exception {e} thrown in match {m}")
							 | 
						||
| 
								 | 
							
								        finally:
							 | 
						||
| 
								 | 
							
								            if dont_restart:  # Keeping them alive after a non-computer match can cause crashes
							 | 
						||
| 
								 | 
							
								                await maintain_SCII_count(0, controllers, m.sc2_config)
							 | 
						||
| 
								 | 
							
								            results.append(result)
							 | 
						||
| 
								 | 
							
								    kill_switch.kill_all()
							 | 
						||
| 
								 | 
							
								    return results
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# TODO Catching too general exception Exception (broad-except)
							 | 
						||
| 
								 | 
							
								# pylint: disable=W0703
							 | 
						||
| 
								 | 
							
								async def a_run_multiple_games_nokill(matches: List[GameMatch]) -> List[Dict[AbstractPlayer, Result]]:
							 | 
						||
| 
								 | 
							
								    """Run multiple matches while reusing SCII processes.
							 | 
						||
| 
								 | 
							
								    Prone to crashes and stalls
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    # FIXME: check whether crashes between bot-vs-bot are avoidable or not
							 | 
						||
| 
								 | 
							
								    if not matches:
							 | 
						||
| 
								 | 
							
								        return []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Start the matches
							 | 
						||
| 
								 | 
							
								    results = []
							 | 
						||
| 
								 | 
							
								    controllers = []
							 | 
						||
| 
								 | 
							
								    for m in matches:
							 | 
						||
| 
								 | 
							
								        logger.info(f"Starting match {1 + len(results)} / {len(matches)}: {m}")
							 | 
						||
| 
								 | 
							
								        result = None
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config)
							 | 
						||
| 
								 | 
							
								            result = await run_match(controllers, m, close_ws=False)
							 | 
						||
| 
								 | 
							
								        except SystemExit as e:
							 | 
						||
| 
								 | 
							
								            logger.critical(f"Game sys.exit'ed as {e} during match {m}")
							 | 
						||
| 
								 | 
							
								        except Exception as e:
							 | 
						||
| 
								 | 
							
								            logger.exception(f"Caught unknown exception: {e}")
							 | 
						||
| 
								 | 
							
								            logger.info(f"Exception {e} thrown in match {m}")
							 | 
						||
| 
								 | 
							
								        finally:
							 | 
						||
| 
								 | 
							
								            for c in controllers:
							 | 
						||
| 
								 | 
							
								                try:
							 | 
						||
| 
								 | 
							
								                    await c.ping()
							 | 
						||
| 
								 | 
							
								                    if c._status != Status.launched:
							 | 
						||
| 
								 | 
							
								                        await c._execute(leave_game=sc_pb.RequestLeaveGame())
							 | 
						||
| 
								 | 
							
								                except Exception as e:
							 | 
						||
| 
								 | 
							
								                    logger.exception(f"Caught unknown exception: {e}")
							 | 
						||
| 
								 | 
							
								                    if not (isinstance(e, ProtocolError) and e.is_game_over_error):
							 | 
						||
| 
								 | 
							
								                        logger.info(f"controller {c.__dict__} threw {e}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            results.append(result)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Fire the killswitch manually, instead of letting the winning player fire it.
							 | 
						||
| 
								 | 
							
								    await asyncio.wait_for(asyncio.gather(*(c._process._close_connection() for c in controllers)), timeout=50)
							 | 
						||
| 
								 | 
							
								    kill_switch.kill_all()
							 | 
						||
| 
								 | 
							
								    signal.signal(signal.SIGINT, signal.SIG_DFL)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return results
							 |