mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	
		
			
	
	
		
			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 |