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
 | 
