├── .gitignore ├── LICENSE ├── README.md ├── bots ├── .gitignore ├── basic_bot │ ├── README.md │ ├── __init__.py │ ├── bot.py │ ├── run.py │ └── sc2 │ │ ├── __init__.py │ │ ├── action.py │ │ ├── bot_ai.py │ │ ├── bot_ai_internal.py │ │ ├── cache.py │ │ ├── client.py │ │ ├── constants.py │ │ ├── controller.py │ │ ├── data.py │ │ ├── dicts │ │ ├── __init__.py │ │ ├── generic_redirect_abilities.py │ │ ├── unit_abilities.py │ │ ├── unit_research_abilities.py │ │ ├── unit_tech_alias.py │ │ ├── unit_train_build_abilities.py │ │ ├── unit_trained_from.py │ │ ├── unit_unit_alias.py │ │ └── upgrade_researched_from.py │ │ ├── expiring_dict.py │ │ ├── game_data.py │ │ ├── game_info.py │ │ ├── game_state.py │ │ ├── generate_ids.py │ │ ├── ids │ │ ├── __init__.py │ │ ├── ability_id.py │ │ ├── buff_id.py │ │ ├── effect_id.py │ │ ├── id_version.py │ │ ├── unit_typeid.py │ │ └── upgrade_id.py │ │ ├── main.py │ │ ├── maps.py │ │ ├── observer_ai.py │ │ ├── paths.py │ │ ├── pixel_map.py │ │ ├── player.py │ │ ├── portconfig.py │ │ ├── position.py │ │ ├── power_source.py │ │ ├── protocol.py │ │ ├── proxy.py │ │ ├── renderer.py │ │ ├── sc2process.py │ │ ├── score.py │ │ ├── unit.py │ │ ├── unit_command.py │ │ ├── units.py │ │ ├── versions.py │ │ └── wsl.py └── loser_bot │ ├── README.md │ ├── __init__.py │ ├── bot.py │ ├── run.py │ └── sc2 │ ├── __init__.py │ ├── action.py │ ├── bot_ai.py │ ├── bot_ai_internal.py │ ├── cache.py │ ├── client.py │ ├── constants.py │ ├── controller.py │ ├── data.py │ ├── dicts │ ├── __init__.py │ ├── generic_redirect_abilities.py │ ├── unit_abilities.py │ ├── unit_research_abilities.py │ ├── unit_tech_alias.py │ ├── unit_train_build_abilities.py │ ├── unit_trained_from.py │ ├── unit_unit_alias.py │ └── upgrade_researched_from.py │ ├── expiring_dict.py │ ├── game_data.py │ ├── game_info.py │ ├── game_state.py │ ├── generate_ids.py │ ├── ids │ ├── __init__.py │ ├── ability_id.py │ ├── buff_id.py │ ├── effect_id.py │ ├── id_version.py │ ├── unit_typeid.py │ └── upgrade_id.py │ ├── main.py │ ├── maps.py │ ├── observer_ai.py │ ├── paths.py │ ├── pixel_map.py │ ├── player.py │ ├── portconfig.py │ ├── position.py │ ├── power_source.py │ ├── protocol.py │ ├── proxy.py │ ├── renderer.py │ ├── sc2process.py │ ├── score.py │ ├── unit.py │ ├── unit_command.py │ ├── units.py │ ├── versions.py │ └── wsl.py ├── config.toml ├── docker-compose-host-network.yml ├── docker-compose-multithread-example.yml ├── docker-compose.yml ├── img └── download.png ├── logs └── .gitignore ├── maps └── AcropolisAIE.SC2Map ├── matches ├── multithread-example.py ├── replays └── .gitignore └── results.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | results 4 | runners -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # local-play-bootstrap 2 | 3 | ## What is this? 4 | This repo intends to be a pain-free method for SC2 AI bot authors to quickly setup and run local bot matches on their system, using the same set of docker images that the [AI Arena ladder](https://aiarena.net) runs on. 5 | 6 | ## Prerequisites 7 | 8 | This bootstrap requires Docker Compose to run. 9 | 10 | 11 | If you've installed Docker on Windows or MacOS then Docker Compose is already installed. 12 | For other systems: [How to install Docker Compose](https://docs.docker.com/compose/install/) 13 | 14 | ## Getting started 15 | 16 | ### Download this repo 17 | ![Download this repo](img/download.png) 18 | 19 | ### Validating your setup 20 | 21 | A test match between 2 included test bots along with the map AcropolisAIE is preconfigured to run in the `matches` file. 22 | 23 | You can run the test match by executing `docker-compose up` in the base folder of this repo. 24 | 25 | ## Running your own matches 26 | 27 | 1. Put your bots in the `./bots` folder. 28 | 2. Download the [latest ladder maps](https://aiarena.net//wiki/maps/#wiki-toc-current-map-pool) and place them in this repo's local `./maps` folder. 29 | See [Use an alternative maps folder location](#use-an-alternative-maps-folder-location) if you want to use a different maps folder. 30 | 2. Add the relevant entries for matches you wish to play to the `matches` file. 31 | 3. Run `docker compose up` to run the matches. 32 | 4. View results in the `results.json` file and replays in the `replays` folder. 33 | 34 | ### Multi-threaded matches 35 | 36 | Refer to [multithread-example.py](./multithread-example.py) for an example of how to run multiple matches in parallel. 37 | 38 | Note that there are aspects of bot games that would need more work to be thread safe, 39 | such as bots which save data to their data folder. 40 | 41 | ## Troubleshooting 42 | 43 | ### Tips 44 | 45 | All container and bot logs can be found in the `logs` folder. 46 | 47 | Docker container output can also be seen after running the `docker compose up` command. 48 | You can also revisit the container output of previous runs by running `docker compose logs`. 49 | 50 | ### Specific scenarios 51 | 52 | #### Bots connecting to localhost using default `docker compose up` command 53 | If you encounter an error message resembling the following: 54 | ``` 55 | 2023-08-01T20:25:25.407722Z ERROR common/src/api/api_reference/mod.rs:228: ResponseError(ResponseContent { status: 400, api_error_message: ApiErrorMessage { error: "Could not find port for started process" } }) 56 | 2023-08-01T20:25:25.407869Z ERROR proxy_controller/src/match_scheduler/mod.rs:209: Failed to start bot 1: error in response: status code 400 Bad Request 57 | Error:ApiErrorMessage { error: "Could not find port for started process" } 58 | ``` 59 | 60 | This signifies that one or both of the bots are attempting to connect to the localhost. 61 | To resolve this issue, consider adjusting the command line arguments on the bot when 62 | connecting to the ladder server. 63 | For instance: 64 | 65 | `Bot.exe --GamePort 8080 --LadderServer 172.18.0.2 --StartPort 8080 --OpponentId ABCD` 66 | 67 | Alternatively if you're unable to fix this or running legacy bots you can utilize the 68 | provided `docker-compose-host-network.yml` file to run matches with the following command: 69 | 70 | `docker compose -f docker-compose-host-network.yml up` 71 | 72 | Please be aware that running multiple games concurrently may not function correctly using this approach. 73 | 74 | ## Use an alternative maps folder location 75 | It can sometimes be handy to have your maps in another location e.g. if you want to use the same maps folder as your StarCraft II installation. 76 | 77 | To do this, update the SC2 Maps Path setting in the docker-compose.yml file to point to your maps folder. 78 | 79 | ## Contribute 80 | If you notice issues with setup, or have ideas that might help other bot authors, please feel free to contribute via a pull request. 81 | 82 | ## License 83 | 84 | Copyright (c) 2022 85 | 86 | Licensed under the [GPLv3 license](LICENSE). 87 | -------------------------------------------------------------------------------- /bots/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /bots/basic_bot/README.md: -------------------------------------------------------------------------------- 1 | ## Test Bot 2 | ### Normal Bot Simulation 3 | 4 | This is a normal bot that does an a-move worker rush on the first frame -------------------------------------------------------------------------------- /bots/basic_bot/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0212 2 | import argparse 3 | import asyncio 4 | 5 | import aiohttp 6 | from loguru import logger 7 | 8 | import sc2 9 | from sc2.client import Client 10 | from sc2.protocol import ConnectionAlreadyClosed 11 | 12 | 13 | # Run ladder game 14 | # This lets python-sc2 connect to a LadderManager game: https://github.com/Cryptyc/Sc2LadderServer 15 | # Based on: https://github.com/Dentosal/python-sc2/blob/master/examples/run_external.py 16 | def run_ladder_game(bot): 17 | # Load command line arguments 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument("--GamePort", type=int, nargs="?", help="Game port") 20 | parser.add_argument("--StartPort", type=int, nargs="?", help="Start port") 21 | parser.add_argument("--LadderServer", type=str, nargs="?", help="Ladder server") 22 | parser.add_argument("--ComputerOpponent", type=str, nargs="?", help="Computer opponent") 23 | parser.add_argument("--ComputerRace", type=str, nargs="?", help="Computer race") 24 | parser.add_argument("--ComputerDifficulty", type=str, nargs="?", help="Computer difficulty") 25 | parser.add_argument("--OpponentId", type=str, nargs="?", help="Opponent ID") 26 | parser.add_argument("--RealTime", action="store_true", help="Real time flag") 27 | args, _unknown = parser.parse_known_args() 28 | 29 | if args.LadderServer is None: 30 | host = "127.0.0.1" 31 | else: 32 | host = args.LadderServer 33 | 34 | host_port = args.GamePort 35 | lan_port = args.StartPort 36 | 37 | # Add opponent_id to the bot class (accessed through self.opponent_id) 38 | bot.ai.opponent_id = args.OpponentId 39 | 40 | realtime = args.RealTime 41 | 42 | # Port config 43 | if lan_port is None: 44 | portconfig = None 45 | else: 46 | ports = [lan_port + p for p in range(1, 6)] 47 | 48 | portconfig = sc2.portconfig.Portconfig() 49 | portconfig.server = [ports[1], ports[2]] 50 | portconfig.players = [[ports[3], ports[4]]] 51 | 52 | # Join ladder game 53 | g = join_ladder_game(host=host, port=host_port, players=[bot], realtime=realtime, portconfig=portconfig) 54 | 55 | # Run it 56 | result = asyncio.get_event_loop().run_until_complete(g) 57 | return result, args.OpponentId 58 | 59 | 60 | # Modified version of sc2.main._join_game to allow custom host and port, and to not spawn an additional sc2process (thanks to alkurbatov for fix) 61 | async def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, game_time_limit=None): 62 | ws_url = f"ws://{host}:{port}/sc2api" 63 | ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120) 64 | client = Client(ws_connection) 65 | try: 66 | result = await sc2.main._play_game(players[0], client, realtime, portconfig, game_time_limit) 67 | if save_replay_as is not None: 68 | await client.save_replay(save_replay_as) 69 | # await client.leave() 70 | # await client.quit() 71 | except ConnectionAlreadyClosed: 72 | logger.error("Connection was closed before the game ended") 73 | return None 74 | finally: 75 | ws_connection.close() 76 | 77 | return result -------------------------------------------------------------------------------- /bots/basic_bot/bot.py: -------------------------------------------------------------------------------- 1 | from sc2.bot_ai import BotAI 2 | from sc2.data import Result 3 | 4 | 5 | class TestBot(BotAI): 6 | async def on_step(self, iteration): 7 | if iteration == 0: 8 | for worker in self.workers: 9 | worker.attack(self.enemy_start_locations[0]) 10 | 11 | self.game_step = 100 12 | -------------------------------------------------------------------------------- /bots/basic_bot/run.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E0401 2 | import sys 3 | 4 | from __init__ import run_ladder_game 5 | 6 | # Load bot 7 | from bot import TestBot 8 | 9 | from sc2 import maps 10 | from sc2.data import Difficulty, Race 11 | from sc2.main import run_game 12 | from sc2.player import Bot, Computer 13 | 14 | bot = Bot(Race.Terran, TestBot()) 15 | 16 | # Start game 17 | if __name__ == "__main__": 18 | if "--LadderServer" in sys.argv: 19 | # Ladder game started by LadderManager 20 | print("Starting ladder game...") 21 | result, opponentid = run_ladder_game(bot) 22 | print(result, " against opponent ", opponentid) 23 | else: 24 | # Local game 25 | print("Starting local game...") 26 | run_game(maps.get("Abyssal Reef LE"), [bot, Computer(Race.Protoss, Difficulty.VeryHard)], realtime=True) 27 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def is_submodule(path): 5 | if path.is_file(): 6 | return path.suffix == ".py" and path.stem != "__init__" 7 | if path.is_dir(): 8 | return (path / "__init__.py").exists() 9 | return False 10 | 11 | 12 | __all__ = [p.stem for p in Path(__file__).parent.iterdir() if is_submodule(p)] 13 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/action.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import groupby 4 | from typing import TYPE_CHECKING, Union 5 | 6 | from s2clientprotocol import raw_pb2 as raw_pb 7 | 8 | from sc2.position import Point2 9 | from sc2.unit import Unit 10 | 11 | if TYPE_CHECKING: 12 | from sc2.ids.ability_id import AbilityId 13 | from sc2.unit_command import UnitCommand 14 | 15 | 16 | # pylint: disable=R0912 17 | def combine_actions(action_iter): 18 | """ 19 | Example input: 20 | [ 21 | # Each entry in the list is a unit command, with an ability, unit, target, and queue=boolean 22 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hive', tag=4353687554), None, False), 23 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Lair', tag=4359979012), None, False), 24 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hatchery', tag=4359454723), None, False), 25 | ] 26 | """ 27 | for key, items in groupby(action_iter, key=lambda a: a.combining_tuple): 28 | ability: AbilityId 29 | target: Union[None, Point2, Unit] 30 | queue: bool 31 | # See constants.py for combineable abilities 32 | combineable: bool 33 | ability, target, queue, combineable = key 34 | 35 | if combineable: 36 | # Combine actions with no target, e.g. lift, burrowup, burrowdown, siege, unsiege, uproot spines 37 | cmd = raw_pb.ActionRawUnitCommand( 38 | ability_id=ability.value, unit_tags={u.unit.tag 39 | for u in items}, queue_command=queue 40 | ) 41 | # Combine actions with target point, e.g. attack_move or move commands on a position 42 | if isinstance(target, Point2): 43 | cmd.target_world_space_pos.x = target.x 44 | cmd.target_world_space_pos.y = target.y 45 | # Combine actions with target unit, e.g. attack commands directly on a unit 46 | elif isinstance(target, Unit): 47 | cmd.target_unit_tag = target.tag 48 | elif target is not None: 49 | raise RuntimeError(f"Must target a unit, point or None, found '{target !r}'") 50 | 51 | yield raw_pb.ActionRaw(unit_command=cmd) 52 | 53 | else: 54 | """ 55 | Return one action for each unit; this is required for certain commands that would otherwise be grouped, and only executed once 56 | Examples: 57 | Select 3 hatcheries, build a queen with each hatch - the grouping function would group these unit tags and only issue one train command once to all 3 unit tags - resulting in one total train command 58 | I imagine the same thing would happen to certain other abilities: Battlecruiser yamato on same target, queen transfuse on same target, ghost snipe on same target, all build commands with the same unit type and also all morphs (zergling to banelings) 59 | However, other abilities can and should be grouped, see constants.py 'COMBINEABLE_ABILITIES' 60 | """ 61 | u: UnitCommand 62 | if target is None: 63 | for u in items: 64 | cmd = raw_pb.ActionRawUnitCommand( 65 | ability_id=ability.value, unit_tags={u.unit.tag}, queue_command=queue 66 | ) 67 | yield raw_pb.ActionRaw(unit_command=cmd) 68 | elif isinstance(target, Point2): 69 | for u in items: 70 | cmd = raw_pb.ActionRawUnitCommand( 71 | ability_id=ability.value, 72 | unit_tags={u.unit.tag}, 73 | queue_command=queue, 74 | target_world_space_pos=target.as_Point2D, 75 | ) 76 | yield raw_pb.ActionRaw(unit_command=cmd) 77 | 78 | elif isinstance(target, Unit): 79 | for u in items: 80 | cmd = raw_pb.ActionRawUnitCommand( 81 | ability_id=ability.value, 82 | unit_tags={u.unit.tag}, 83 | queue_command=queue, 84 | target_unit_tag=target.tag, 85 | ) 86 | yield raw_pb.ActionRaw(unit_command=cmd) 87 | else: 88 | raise RuntimeError(f"Must target a unit, point or None, found '{target !r}'") 89 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Callable, TypeVar 4 | 5 | if TYPE_CHECKING: 6 | from sc2.bot_ai import BotAI 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | class property_cache_once_per_frame(property): 12 | """This decorator caches the return value for one game loop, 13 | then clears it if it is accessed in a different game loop. 14 | Only works on properties of the bot object, because it requires 15 | access to self.state.game_loop 16 | 17 | This decorator compared to the above runs a little faster, however you should only use this decorator if you are sure that you do not modify the mutable once it is calculated and cached. 18 | 19 | Copied and modified from https://tedboy.github.io/flask/_modules/werkzeug/utils.html#cached_property 20 | # """ 21 | 22 | def __init__(self, func: Callable[[BotAI], T], name=None): 23 | # pylint: disable=W0231 24 | self.__name__ = name or func.__name__ 25 | self.__frame__ = f"__frame__{self.__name__}" 26 | self.func = func 27 | 28 | def __set__(self, obj: BotAI, value: T): 29 | obj.cache[self.__name__] = value 30 | obj.cache[self.__frame__] = obj.state.game_loop 31 | 32 | def __get__(self, obj: BotAI, _type=None) -> T: 33 | value = obj.cache.get(self.__name__, None) 34 | bot_frame = obj.state.game_loop 35 | if value is None or obj.cache[self.__frame__] < bot_frame: 36 | value = self.func(obj) 37 | obj.cache[self.__name__] = value 38 | obj.cache[self.__frame__] = bot_frame 39 | return value 40 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/controller.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | from s2clientprotocol import sc2api_pb2 as sc_pb 6 | 7 | from sc2.player import Computer 8 | from sc2.protocol import Protocol 9 | 10 | 11 | class Controller(Protocol): 12 | 13 | def __init__(self, ws, process): 14 | super().__init__(ws) 15 | self._process = process 16 | 17 | @property 18 | def running(self): 19 | # pylint: disable=W0212 20 | return self._process._process is not None 21 | 22 | async def create_game(self, game_map, players, realtime: bool, random_seed=None, disable_fog=None): 23 | req = sc_pb.RequestCreateGame( 24 | local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)), realtime=realtime, disable_fog=disable_fog 25 | ) 26 | if random_seed is not None: 27 | req.random_seed = random_seed 28 | 29 | for player in players: 30 | p = req.player_setup.add() 31 | p.type = player.type.value 32 | if isinstance(player, Computer): 33 | p.race = player.race.value 34 | p.difficulty = player.difficulty.value 35 | p.ai_build = player.ai_build.value 36 | 37 | logger.info("Creating new game") 38 | logger.info(f"Map: {game_map.name}") 39 | logger.info(f"Players: {', '.join(str(p) for p in players)}") 40 | result = await self._execute(create_game=req) 41 | return result 42 | 43 | async def request_available_maps(self): 44 | req = sc_pb.RequestAvailableMaps() 45 | result = await self._execute(available_maps=req) 46 | return result 47 | 48 | async def request_save_map(self, download_path: str): 49 | """ Not working on linux. """ 50 | req = sc_pb.RequestSaveMap(map_path=download_path) 51 | result = await self._execute(save_map=req) 52 | return result 53 | 54 | async def request_replay_info(self, replay_path: str): 55 | """ Not working on linux. """ 56 | req = sc_pb.RequestReplayInfo(replay_path=replay_path, download_data=False) 57 | result = await self._execute(replay_info=req) 58 | return result 59 | 60 | async def start_replay(self, replay_path: str, realtime: bool, observed_id: int = 0): 61 | ifopts = sc_pb.InterfaceOptions( 62 | raw=True, score=True, show_cloaked=True, raw_affects_selection=True, raw_crop_to_playable_area=False 63 | ) 64 | if platform.system() == "Linux": 65 | replay_name = Path(replay_path).name 66 | home_replay_folder = Path.home() / "Documents" / "StarCraft II" / "Replays" 67 | if str(home_replay_folder / replay_name) != replay_path: 68 | logger.warning( 69 | f"Linux detected, please put your replay in your home directory at {home_replay_folder}. It was detected at {replay_path}" 70 | ) 71 | raise FileNotFoundError 72 | replay_path = replay_name 73 | 74 | req = sc_pb.RequestStartReplay( 75 | replay_path=replay_path, observed_player_id=observed_id, realtime=realtime, options=ifopts 76 | ) 77 | 78 | result = await self._execute(start_replay=req) 79 | assert result.status == 4, f"{result.start_replay.error} - {result.start_replay.error_details}" 80 | return result 81 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/data.py: -------------------------------------------------------------------------------- 1 | """ For the list of enums, see here 2 | 3 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_gametypes.h 4 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_action.h 5 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_unit.h 6 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_data.h 7 | """ 8 | import enum 9 | from typing import Dict, Set 10 | 11 | from s2clientprotocol import common_pb2 as common_pb 12 | from s2clientprotocol import data_pb2 as data_pb 13 | from s2clientprotocol import error_pb2 as error_pb 14 | from s2clientprotocol import raw_pb2 as raw_pb 15 | from s2clientprotocol import sc2api_pb2 as sc_pb 16 | 17 | from sc2.ids.ability_id import AbilityId 18 | from sc2.ids.unit_typeid import UnitTypeId 19 | 20 | CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items()) 21 | 22 | PlayerType = enum.Enum("PlayerType", sc_pb.PlayerType.items()) 23 | Difficulty = enum.Enum("Difficulty", sc_pb.Difficulty.items()) 24 | AIBuild = enum.Enum("AIBuild", sc_pb.AIBuild.items()) 25 | Status = enum.Enum("Status", sc_pb.Status.items()) 26 | Result = enum.Enum("Result", sc_pb.Result.items()) 27 | Alert = enum.Enum("Alert", sc_pb.Alert.items()) 28 | ChatChannel = enum.Enum("ChatChannel", sc_pb.ActionChat.Channel.items()) 29 | 30 | Race = enum.Enum("Race", common_pb.Race.items()) 31 | 32 | DisplayType = enum.Enum("DisplayType", raw_pb.DisplayType.items()) 33 | Alliance = enum.Enum("Alliance", raw_pb.Alliance.items()) 34 | CloakState = enum.Enum("CloakState", raw_pb.CloakState.items()) 35 | 36 | Attribute = enum.Enum("Attribute", data_pb.Attribute.items()) 37 | TargetType = enum.Enum("TargetType", data_pb.Weapon.TargetType.items()) 38 | Target = enum.Enum("Target", data_pb.AbilityData.Target.items()) 39 | 40 | ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items()) 41 | 42 | race_worker: Dict[Race, UnitTypeId] = { 43 | Race.Protoss: UnitTypeId.PROBE, 44 | Race.Terran: UnitTypeId.SCV, 45 | Race.Zerg: UnitTypeId.DRONE, 46 | } 47 | 48 | race_townhalls: Dict[Race, Set[UnitTypeId]] = { 49 | Race.Protoss: {UnitTypeId.NEXUS}, 50 | Race.Terran: { 51 | UnitTypeId.COMMANDCENTER, 52 | UnitTypeId.ORBITALCOMMAND, 53 | UnitTypeId.PLANETARYFORTRESS, 54 | UnitTypeId.COMMANDCENTERFLYING, 55 | UnitTypeId.ORBITALCOMMANDFLYING, 56 | }, 57 | Race.Zerg: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, 58 | Race.Random: { 59 | # Protoss 60 | UnitTypeId.NEXUS, 61 | # Terran 62 | UnitTypeId.COMMANDCENTER, 63 | UnitTypeId.ORBITALCOMMAND, 64 | UnitTypeId.PLANETARYFORTRESS, 65 | UnitTypeId.COMMANDCENTERFLYING, 66 | UnitTypeId.ORBITALCOMMANDFLYING, 67 | # Zerg 68 | UnitTypeId.HATCHERY, 69 | UnitTypeId.LAIR, 70 | UnitTypeId.HIVE, 71 | }, 72 | } 73 | 74 | warpgate_abilities: Dict[AbilityId, AbilityId] = { 75 | AbilityId.GATEWAYTRAIN_ZEALOT: AbilityId.WARPGATETRAIN_ZEALOT, 76 | AbilityId.GATEWAYTRAIN_STALKER: AbilityId.WARPGATETRAIN_STALKER, 77 | AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: AbilityId.WARPGATETRAIN_HIGHTEMPLAR, 78 | AbilityId.GATEWAYTRAIN_DARKTEMPLAR: AbilityId.WARPGATETRAIN_DARKTEMPLAR, 79 | AbilityId.GATEWAYTRAIN_SENTRY: AbilityId.WARPGATETRAIN_SENTRY, 80 | AbilityId.TRAIN_ADEPT: AbilityId.TRAINWARP_ADEPT, 81 | } 82 | 83 | race_gas: Dict[Race, UnitTypeId] = { 84 | Race.Protoss: UnitTypeId.ASSIMILATOR, 85 | Race.Terran: UnitTypeId.REFINERY, 86 | Race.Zerg: UnitTypeId.EXTRACTOR, 87 | } 88 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/dicts/__init__.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_dicts_from_data_json.py" 3 | 4 | __all__ = [ 5 | 'generic_redirect_abilities', 'unit_abilities', 'unit_research_abilities', 'unit_tech_alias', 6 | 'unit_train_build_abilities', 'unit_trained_from', 'unit_unit_alias', 'upgrade_researched_from' 7 | ] 8 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/dicts/unit_tech_alias.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict, Set 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_TECH_ALIAS: Dict[UnitTypeId, Set[UnitTypeId]] = { 12 | UnitTypeId.BARRACKSFLYING: {UnitTypeId.BARRACKS}, 13 | UnitTypeId.BARRACKSREACTOR: {UnitTypeId.REACTOR}, 14 | UnitTypeId.BARRACKSTECHLAB: {UnitTypeId.TECHLAB}, 15 | UnitTypeId.COMMANDCENTERFLYING: {UnitTypeId.COMMANDCENTER}, 16 | UnitTypeId.CREEPTUMORBURROWED: {UnitTypeId.CREEPTUMOR}, 17 | UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.CREEPTUMOR}, 18 | UnitTypeId.FACTORYFLYING: {UnitTypeId.FACTORY}, 19 | UnitTypeId.FACTORYREACTOR: {UnitTypeId.REACTOR}, 20 | UnitTypeId.FACTORYTECHLAB: {UnitTypeId.TECHLAB}, 21 | UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, 22 | UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR}, 23 | UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, 24 | UnitTypeId.LIBERATORAG: {UnitTypeId.LIBERATOR}, 25 | UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, 26 | UnitTypeId.ORBITALCOMMANDFLYING: {UnitTypeId.COMMANDCENTER}, 27 | UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, 28 | UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD}, 29 | UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.OVERLORD}, 30 | UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, 31 | UnitTypeId.PYLONOVERCHARGED: {UnitTypeId.PYLON}, 32 | UnitTypeId.QUEENBURROWED: {UnitTypeId.QUEEN}, 33 | UnitTypeId.SIEGETANKSIEGED: {UnitTypeId.SIEGETANK}, 34 | UnitTypeId.STARPORTFLYING: {UnitTypeId.STARPORT}, 35 | UnitTypeId.STARPORTREACTOR: {UnitTypeId.REACTOR}, 36 | UnitTypeId.STARPORTTECHLAB: {UnitTypeId.TECHLAB}, 37 | UnitTypeId.SUPPLYDEPOTLOWERED: {UnitTypeId.SUPPLYDEPOT}, 38 | UnitTypeId.THORAP: {UnitTypeId.THOR}, 39 | UnitTypeId.VIKINGASSAULT: {UnitTypeId.VIKING}, 40 | UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKING}, 41 | UnitTypeId.WARPGATE: {UnitTypeId.GATEWAY}, 42 | UnitTypeId.WARPPRISMPHASING: {UnitTypeId.WARPPRISM}, 43 | UnitTypeId.WIDOWMINEBURROWED: {UnitTypeId.WIDOWMINE} 44 | } 45 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/dicts/unit_trained_from.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict, Set 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_TRAINED_FROM: Dict[UnitTypeId, Set[UnitTypeId]] = { 12 | UnitTypeId.ADEPT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 13 | UnitTypeId.ARMORY: {UnitTypeId.SCV}, 14 | UnitTypeId.ASSIMILATOR: {UnitTypeId.PROBE}, 15 | UnitTypeId.AUTOTURRET: {UnitTypeId.RAVEN}, 16 | UnitTypeId.BANELING: {UnitTypeId.ZERGLING}, 17 | UnitTypeId.BANELINGNEST: {UnitTypeId.DRONE}, 18 | UnitTypeId.BANSHEE: {UnitTypeId.STARPORT}, 19 | UnitTypeId.BARRACKS: {UnitTypeId.SCV}, 20 | UnitTypeId.BATTLECRUISER: {UnitTypeId.STARPORT}, 21 | UnitTypeId.BROODLORD: {UnitTypeId.CORRUPTOR}, 22 | UnitTypeId.BUNKER: {UnitTypeId.SCV}, 23 | UnitTypeId.CARRIER: {UnitTypeId.STARGATE}, 24 | UnitTypeId.CHANGELING: {UnitTypeId.OVERSEER, UnitTypeId.OVERSEERSIEGEMODE}, 25 | UnitTypeId.COLOSSUS: {UnitTypeId.ROBOTICSFACILITY}, 26 | UnitTypeId.COMMANDCENTER: {UnitTypeId.SCV}, 27 | UnitTypeId.CORRUPTOR: {UnitTypeId.LARVA}, 28 | UnitTypeId.CREEPTUMOR: { 29 | UnitTypeId.CREEPTUMOR, UnitTypeId.CREEPTUMORBURROWED, UnitTypeId.CREEPTUMORQUEEN, UnitTypeId.QUEEN 30 | }, 31 | UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.QUEEN}, 32 | UnitTypeId.CYBERNETICSCORE: {UnitTypeId.PROBE}, 33 | UnitTypeId.CYCLONE: {UnitTypeId.FACTORY}, 34 | UnitTypeId.DARKSHRINE: {UnitTypeId.PROBE}, 35 | UnitTypeId.DARKTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 36 | UnitTypeId.DISRUPTOR: {UnitTypeId.ROBOTICSFACILITY}, 37 | UnitTypeId.DRONE: {UnitTypeId.LARVA}, 38 | UnitTypeId.ENGINEERINGBAY: {UnitTypeId.SCV}, 39 | UnitTypeId.EVOLUTIONCHAMBER: {UnitTypeId.DRONE}, 40 | UnitTypeId.EXTRACTOR: {UnitTypeId.DRONE}, 41 | UnitTypeId.FACTORY: {UnitTypeId.SCV}, 42 | UnitTypeId.FLEETBEACON: {UnitTypeId.PROBE}, 43 | UnitTypeId.FORGE: {UnitTypeId.PROBE}, 44 | UnitTypeId.FUSIONCORE: {UnitTypeId.SCV}, 45 | UnitTypeId.GATEWAY: {UnitTypeId.PROBE}, 46 | UnitTypeId.GHOST: {UnitTypeId.BARRACKS}, 47 | UnitTypeId.GHOSTACADEMY: {UnitTypeId.SCV}, 48 | UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, 49 | UnitTypeId.HATCHERY: {UnitTypeId.DRONE}, 50 | UnitTypeId.HELLION: {UnitTypeId.FACTORY}, 51 | UnitTypeId.HELLIONTANK: {UnitTypeId.FACTORY}, 52 | UnitTypeId.HIGHTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 53 | UnitTypeId.HIVE: {UnitTypeId.LAIR}, 54 | UnitTypeId.HYDRALISK: {UnitTypeId.LARVA}, 55 | UnitTypeId.HYDRALISKDEN: {UnitTypeId.DRONE}, 56 | UnitTypeId.IMMORTAL: {UnitTypeId.ROBOTICSFACILITY}, 57 | UnitTypeId.INFESTATIONPIT: {UnitTypeId.DRONE}, 58 | UnitTypeId.INFESTOR: {UnitTypeId.LARVA}, 59 | UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, 60 | UnitTypeId.LIBERATOR: {UnitTypeId.STARPORT}, 61 | UnitTypeId.LOCUSTMPFLYING: {UnitTypeId.SWARMHOSTBURROWEDMP, UnitTypeId.SWARMHOSTMP}, 62 | UnitTypeId.LURKERDENMP: {UnitTypeId.DRONE}, 63 | UnitTypeId.LURKERMP: {UnitTypeId.HYDRALISK}, 64 | UnitTypeId.MARAUDER: {UnitTypeId.BARRACKS}, 65 | UnitTypeId.MARINE: {UnitTypeId.BARRACKS}, 66 | UnitTypeId.MEDIVAC: {UnitTypeId.STARPORT}, 67 | UnitTypeId.MISSILETURRET: {UnitTypeId.SCV}, 68 | UnitTypeId.MOTHERSHIP: {UnitTypeId.NEXUS}, 69 | UnitTypeId.MUTALISK: {UnitTypeId.LARVA}, 70 | UnitTypeId.NEXUS: {UnitTypeId.PROBE}, 71 | UnitTypeId.NYDUSCANAL: {UnitTypeId.NYDUSNETWORK}, 72 | UnitTypeId.NYDUSNETWORK: {UnitTypeId.DRONE}, 73 | UnitTypeId.OBSERVER: {UnitTypeId.ROBOTICSFACILITY}, 74 | UnitTypeId.ORACLE: {UnitTypeId.STARGATE}, 75 | UnitTypeId.ORACLESTASISTRAP: {UnitTypeId.ORACLE}, 76 | UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, 77 | UnitTypeId.OVERLORD: {UnitTypeId.LARVA}, 78 | UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, 79 | UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD, UnitTypeId.OVERLORDTRANSPORT}, 80 | UnitTypeId.PHOENIX: {UnitTypeId.STARGATE}, 81 | UnitTypeId.PHOTONCANNON: {UnitTypeId.PROBE}, 82 | UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, 83 | UnitTypeId.PROBE: {UnitTypeId.NEXUS}, 84 | UnitTypeId.PYLON: {UnitTypeId.PROBE}, 85 | UnitTypeId.QUEEN: {UnitTypeId.HATCHERY, UnitTypeId.HIVE, UnitTypeId.LAIR}, 86 | UnitTypeId.RAVAGER: {UnitTypeId.ROACH}, 87 | UnitTypeId.RAVEN: {UnitTypeId.STARPORT}, 88 | UnitTypeId.REAPER: {UnitTypeId.BARRACKS}, 89 | UnitTypeId.REFINERY: {UnitTypeId.SCV}, 90 | UnitTypeId.ROACH: {UnitTypeId.LARVA}, 91 | UnitTypeId.ROACHWARREN: {UnitTypeId.DRONE}, 92 | UnitTypeId.ROBOTICSBAY: {UnitTypeId.PROBE}, 93 | UnitTypeId.ROBOTICSFACILITY: {UnitTypeId.PROBE}, 94 | UnitTypeId.SCV: {UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS}, 95 | UnitTypeId.SENSORTOWER: {UnitTypeId.SCV}, 96 | UnitTypeId.SENTRY: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 97 | UnitTypeId.SHIELDBATTERY: {UnitTypeId.PROBE}, 98 | UnitTypeId.SIEGETANK: {UnitTypeId.FACTORY}, 99 | UnitTypeId.SPAWNINGPOOL: {UnitTypeId.DRONE}, 100 | UnitTypeId.SPINECRAWLER: {UnitTypeId.DRONE}, 101 | UnitTypeId.SPIRE: {UnitTypeId.DRONE}, 102 | UnitTypeId.SPORECRAWLER: {UnitTypeId.DRONE}, 103 | UnitTypeId.STALKER: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 104 | UnitTypeId.STARGATE: {UnitTypeId.PROBE}, 105 | UnitTypeId.STARPORT: {UnitTypeId.SCV}, 106 | UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SCV}, 107 | UnitTypeId.SWARMHOSTMP: {UnitTypeId.LARVA}, 108 | UnitTypeId.TEMPEST: {UnitTypeId.STARGATE}, 109 | UnitTypeId.TEMPLARARCHIVE: {UnitTypeId.PROBE}, 110 | UnitTypeId.THOR: {UnitTypeId.FACTORY}, 111 | UnitTypeId.TWILIGHTCOUNCIL: {UnitTypeId.PROBE}, 112 | UnitTypeId.ULTRALISK: {UnitTypeId.LARVA}, 113 | UnitTypeId.ULTRALISKCAVERN: {UnitTypeId.DRONE}, 114 | UnitTypeId.VIKINGFIGHTER: {UnitTypeId.STARPORT}, 115 | UnitTypeId.VIPER: {UnitTypeId.LARVA}, 116 | UnitTypeId.VOIDRAY: {UnitTypeId.STARGATE}, 117 | UnitTypeId.WARPPRISM: {UnitTypeId.ROBOTICSFACILITY}, 118 | UnitTypeId.WIDOWMINE: {UnitTypeId.FACTORY}, 119 | UnitTypeId.ZEALOT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 120 | UnitTypeId.ZERGLING: {UnitTypeId.LARVA} 121 | } 122 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/dicts/unit_unit_alias.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_UNIT_ALIAS: Dict[UnitTypeId, UnitTypeId] = { 12 | UnitTypeId.ADEPTPHASESHIFT: UnitTypeId.ADEPT, 13 | UnitTypeId.BANELINGBURROWED: UnitTypeId.BANELING, 14 | UnitTypeId.BARRACKSFLYING: UnitTypeId.BARRACKS, 15 | UnitTypeId.CHANGELINGMARINE: UnitTypeId.CHANGELING, 16 | UnitTypeId.CHANGELINGMARINESHIELD: UnitTypeId.CHANGELING, 17 | UnitTypeId.CHANGELINGZEALOT: UnitTypeId.CHANGELING, 18 | UnitTypeId.CHANGELINGZERGLING: UnitTypeId.CHANGELING, 19 | UnitTypeId.CHANGELINGZERGLINGWINGS: UnitTypeId.CHANGELING, 20 | UnitTypeId.COMMANDCENTERFLYING: UnitTypeId.COMMANDCENTER, 21 | UnitTypeId.CREEPTUMORBURROWED: UnitTypeId.CREEPTUMOR, 22 | UnitTypeId.CREEPTUMORQUEEN: UnitTypeId.CREEPTUMOR, 23 | UnitTypeId.DRONEBURROWED: UnitTypeId.DRONE, 24 | UnitTypeId.FACTORYFLYING: UnitTypeId.FACTORY, 25 | UnitTypeId.GHOSTNOVA: UnitTypeId.GHOST, 26 | UnitTypeId.HERCPLACEMENT: UnitTypeId.HERC, 27 | UnitTypeId.HYDRALISKBURROWED: UnitTypeId.HYDRALISK, 28 | UnitTypeId.INFESTORBURROWED: UnitTypeId.INFESTOR, 29 | UnitTypeId.INFESTORTERRANBURROWED: UnitTypeId.INFESTORTERRAN, 30 | UnitTypeId.LIBERATORAG: UnitTypeId.LIBERATOR, 31 | UnitTypeId.LOCUSTMPFLYING: UnitTypeId.LOCUSTMP, 32 | UnitTypeId.LURKERMPBURROWED: UnitTypeId.LURKERMP, 33 | UnitTypeId.OBSERVERSIEGEMODE: UnitTypeId.OBSERVER, 34 | UnitTypeId.ORBITALCOMMANDFLYING: UnitTypeId.ORBITALCOMMAND, 35 | UnitTypeId.OVERSEERSIEGEMODE: UnitTypeId.OVERSEER, 36 | UnitTypeId.PYLONOVERCHARGED: UnitTypeId.PYLON, 37 | UnitTypeId.QUEENBURROWED: UnitTypeId.QUEEN, 38 | UnitTypeId.RAVAGERBURROWED: UnitTypeId.RAVAGER, 39 | UnitTypeId.ROACHBURROWED: UnitTypeId.ROACH, 40 | UnitTypeId.SIEGETANKSIEGED: UnitTypeId.SIEGETANK, 41 | UnitTypeId.SPINECRAWLERUPROOTED: UnitTypeId.SPINECRAWLER, 42 | UnitTypeId.SPORECRAWLERUPROOTED: UnitTypeId.SPORECRAWLER, 43 | UnitTypeId.STARPORTFLYING: UnitTypeId.STARPORT, 44 | UnitTypeId.SUPPLYDEPOTLOWERED: UnitTypeId.SUPPLYDEPOT, 45 | UnitTypeId.SWARMHOSTBURROWEDMP: UnitTypeId.SWARMHOSTMP, 46 | UnitTypeId.THORAP: UnitTypeId.THOR, 47 | UnitTypeId.ULTRALISKBURROWED: UnitTypeId.ULTRALISK, 48 | UnitTypeId.VIKINGASSAULT: UnitTypeId.VIKINGFIGHTER, 49 | UnitTypeId.WARPPRISMPHASING: UnitTypeId.WARPPRISM, 50 | UnitTypeId.WIDOWMINEBURROWED: UnitTypeId.WIDOWMINE, 51 | UnitTypeId.ZERGLINGBURROWED: UnitTypeId.ZERGLING 52 | } 53 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/dicts/upgrade_researched_from.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | from sc2.ids.upgrade_id import UpgradeId 8 | 9 | # from ..ids.buff_id import BuffId 10 | # from ..ids.effect_id import EffectId 11 | 12 | UPGRADE_RESEARCHED_FROM: Dict[UpgradeId, UnitTypeId] = { 13 | UpgradeId.ADEPTPIERCINGATTACK: UnitTypeId.TWILIGHTCOUNCIL, 14 | UpgradeId.ANABOLICSYNTHESIS: UnitTypeId.ULTRALISKCAVERN, 15 | UpgradeId.BANSHEECLOAK: UnitTypeId.STARPORTTECHLAB, 16 | UpgradeId.BANSHEESPEED: UnitTypeId.STARPORTTECHLAB, 17 | UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: UnitTypeId.FUSIONCORE, 18 | UpgradeId.BLINKTECH: UnitTypeId.TWILIGHTCOUNCIL, 19 | UpgradeId.BURROW: UnitTypeId.HATCHERY, 20 | UpgradeId.CENTRIFICALHOOKS: UnitTypeId.BANELINGNEST, 21 | UpgradeId.CHARGE: UnitTypeId.TWILIGHTCOUNCIL, 22 | UpgradeId.CHITINOUSPLATING: UnitTypeId.ULTRALISKCAVERN, 23 | UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: UnitTypeId.FACTORYTECHLAB, 24 | UpgradeId.DARKTEMPLARBLINKUPGRADE: UnitTypeId.DARKSHRINE, 25 | UpgradeId.DIGGINGCLAWS: UnitTypeId.LURKERDENMP, 26 | UpgradeId.DRILLCLAWS: UnitTypeId.FACTORYTECHLAB, 27 | UpgradeId.ENHANCEDSHOCKWAVES: UnitTypeId.GHOSTACADEMY, 28 | UpgradeId.EVOLVEGROOVEDSPINES: UnitTypeId.HYDRALISKDEN, 29 | UpgradeId.EVOLVEMUSCULARAUGMENTS: UnitTypeId.HYDRALISKDEN, 30 | UpgradeId.EXTENDEDTHERMALLANCE: UnitTypeId.ROBOTICSBAY, 31 | UpgradeId.GLIALRECONSTITUTION: UnitTypeId.ROACHWARREN, 32 | UpgradeId.GRAVITICDRIVE: UnitTypeId.ROBOTICSBAY, 33 | UpgradeId.HIGHCAPACITYBARRELS: UnitTypeId.FACTORYTECHLAB, 34 | UpgradeId.HISECAUTOTRACKING: UnitTypeId.ENGINEERINGBAY, 35 | UpgradeId.INFESTORENERGYUPGRADE: UnitTypeId.INFESTATIONPIT, 36 | UpgradeId.LIBERATORAGRANGEUPGRADE: UnitTypeId.FUSIONCORE, 37 | UpgradeId.LURKERRANGE: UnitTypeId.LURKERDENMP, 38 | UpgradeId.MEDIVACINCREASESPEEDBOOST: UnitTypeId.FUSIONCORE, 39 | UpgradeId.NEURALPARASITE: UnitTypeId.INFESTATIONPIT, 40 | UpgradeId.OBSERVERGRAVITICBOOSTER: UnitTypeId.ROBOTICSBAY, 41 | UpgradeId.OVERLORDSPEED: UnitTypeId.HATCHERY, 42 | UpgradeId.PERSONALCLOAKING: UnitTypeId.GHOSTACADEMY, 43 | UpgradeId.PHOENIXRANGEUPGRADE: UnitTypeId.FLEETBEACON, 44 | UpgradeId.PROTOSSAIRARMORSLEVEL1: UnitTypeId.CYBERNETICSCORE, 45 | UpgradeId.PROTOSSAIRARMORSLEVEL2: UnitTypeId.CYBERNETICSCORE, 46 | UpgradeId.PROTOSSAIRARMORSLEVEL3: UnitTypeId.CYBERNETICSCORE, 47 | UpgradeId.PROTOSSAIRWEAPONSLEVEL1: UnitTypeId.CYBERNETICSCORE, 48 | UpgradeId.PROTOSSAIRWEAPONSLEVEL2: UnitTypeId.CYBERNETICSCORE, 49 | UpgradeId.PROTOSSAIRWEAPONSLEVEL3: UnitTypeId.CYBERNETICSCORE, 50 | UpgradeId.PROTOSSGROUNDARMORSLEVEL1: UnitTypeId.FORGE, 51 | UpgradeId.PROTOSSGROUNDARMORSLEVEL2: UnitTypeId.FORGE, 52 | UpgradeId.PROTOSSGROUNDARMORSLEVEL3: UnitTypeId.FORGE, 53 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: UnitTypeId.FORGE, 54 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: UnitTypeId.FORGE, 55 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: UnitTypeId.FORGE, 56 | UpgradeId.PROTOSSSHIELDSLEVEL1: UnitTypeId.FORGE, 57 | UpgradeId.PROTOSSSHIELDSLEVEL2: UnitTypeId.FORGE, 58 | UpgradeId.PROTOSSSHIELDSLEVEL3: UnitTypeId.FORGE, 59 | UpgradeId.PSISTORMTECH: UnitTypeId.TEMPLARARCHIVE, 60 | UpgradeId.PUNISHERGRENADES: UnitTypeId.BARRACKSTECHLAB, 61 | UpgradeId.RAVENCORVIDREACTOR: UnitTypeId.STARPORTTECHLAB, 62 | UpgradeId.SHIELDWALL: UnitTypeId.BARRACKSTECHLAB, 63 | UpgradeId.SMARTSERVOS: UnitTypeId.FACTORYTECHLAB, 64 | UpgradeId.STIMPACK: UnitTypeId.BARRACKSTECHLAB, 65 | UpgradeId.TEMPESTGROUNDATTACKUPGRADE: UnitTypeId.FLEETBEACON, 66 | UpgradeId.TERRANBUILDINGARMOR: UnitTypeId.ENGINEERINGBAY, 67 | UpgradeId.TERRANINFANTRYARMORSLEVEL1: UnitTypeId.ENGINEERINGBAY, 68 | UpgradeId.TERRANINFANTRYARMORSLEVEL2: UnitTypeId.ENGINEERINGBAY, 69 | UpgradeId.TERRANINFANTRYARMORSLEVEL3: UnitTypeId.ENGINEERINGBAY, 70 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: UnitTypeId.ENGINEERINGBAY, 71 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: UnitTypeId.ENGINEERINGBAY, 72 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: UnitTypeId.ENGINEERINGBAY, 73 | UpgradeId.TERRANSHIPWEAPONSLEVEL1: UnitTypeId.ARMORY, 74 | UpgradeId.TERRANSHIPWEAPONSLEVEL2: UnitTypeId.ARMORY, 75 | UpgradeId.TERRANSHIPWEAPONSLEVEL3: UnitTypeId.ARMORY, 76 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: UnitTypeId.ARMORY, 77 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: UnitTypeId.ARMORY, 78 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: UnitTypeId.ARMORY, 79 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: UnitTypeId.ARMORY, 80 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: UnitTypeId.ARMORY, 81 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: UnitTypeId.ARMORY, 82 | UpgradeId.TUNNELINGCLAWS: UnitTypeId.ROACHWARREN, 83 | UpgradeId.VOIDRAYSPEEDUPGRADE: UnitTypeId.FLEETBEACON, 84 | UpgradeId.WARPGATERESEARCH: UnitTypeId.CYBERNETICSCORE, 85 | UpgradeId.ZERGFLYERARMORSLEVEL1: UnitTypeId.SPIRE, 86 | UpgradeId.ZERGFLYERARMORSLEVEL2: UnitTypeId.SPIRE, 87 | UpgradeId.ZERGFLYERARMORSLEVEL3: UnitTypeId.SPIRE, 88 | UpgradeId.ZERGFLYERWEAPONSLEVEL1: UnitTypeId.SPIRE, 89 | UpgradeId.ZERGFLYERWEAPONSLEVEL2: UnitTypeId.SPIRE, 90 | UpgradeId.ZERGFLYERWEAPONSLEVEL3: UnitTypeId.SPIRE, 91 | UpgradeId.ZERGGROUNDARMORSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 92 | UpgradeId.ZERGGROUNDARMORSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 93 | UpgradeId.ZERGGROUNDARMORSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, 94 | UpgradeId.ZERGLINGATTACKSPEED: UnitTypeId.SPAWNINGPOOL, 95 | UpgradeId.ZERGLINGMOVEMENTSPEED: UnitTypeId.SPAWNINGPOOL, 96 | UpgradeId.ZERGMELEEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 97 | UpgradeId.ZERGMELEEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 98 | UpgradeId.ZERGMELEEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, 99 | UpgradeId.ZERGMISSILEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 100 | UpgradeId.ZERGMISSILEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 101 | UpgradeId.ZERGMISSILEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER 102 | } 103 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/expiring_dict.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import OrderedDict 4 | from threading import RLock 5 | from typing import TYPE_CHECKING, Any, Iterable, Union 6 | 7 | if TYPE_CHECKING: 8 | from sc2.bot_ai import BotAI 9 | 10 | 11 | class ExpiringDict(OrderedDict): 12 | """ 13 | An expiring dict that uses the bot.state.game_loop to only return items that are valid for a specific amount of time. 14 | 15 | Example usages:: 16 | 17 | async def on_step(iteration: int): 18 | # This dict will hold up to 10 items and only return values that have been added up to 20 frames ago 19 | my_dict = ExpiringDict(self, max_age_frames=20) 20 | if iteration == 0: 21 | # Add item 22 | my_dict["test"] = "something" 23 | if iteration == 2: 24 | # On default, one iteration is called every 8 frames 25 | if "test" in my_dict: 26 | print("test is in dict") 27 | if iteration == 20: 28 | if "test" not in my_dict: 29 | print("test is not anymore in dict") 30 | """ 31 | 32 | def __init__(self, bot: BotAI, max_age_frames: int = 1): 33 | assert max_age_frames >= -1 34 | assert bot 35 | 36 | OrderedDict.__init__(self) 37 | self.bot: BotAI = bot 38 | self.max_age: Union[int, float] = max_age_frames 39 | self.lock: RLock = RLock() 40 | 41 | @property 42 | def frame(self) -> int: 43 | return self.bot.state.game_loop 44 | 45 | def __contains__(self, key) -> bool: 46 | """ Return True if dict has key, else False, e.g. 'key in dict' """ 47 | with self.lock: 48 | if OrderedDict.__contains__(self, key): 49 | # Each item is a list of [value, frame time] 50 | item = OrderedDict.__getitem__(self, key) 51 | if self.frame - item[1] < self.max_age: 52 | return True 53 | del self[key] 54 | return False 55 | 56 | def __getitem__(self, key, with_age=False) -> Any: 57 | """ Return the item of the dict using d[key] """ 58 | with self.lock: 59 | # Each item is a list of [value, frame time] 60 | item = OrderedDict.__getitem__(self, key) 61 | if self.frame - item[1] < self.max_age: 62 | if with_age: 63 | return item[0], item[1] 64 | return item[0] 65 | OrderedDict.__delitem__(self, key) 66 | raise KeyError(key) 67 | 68 | def __setitem__(self, key, value): 69 | """ Set d[key] = value """ 70 | with self.lock: 71 | OrderedDict.__setitem__(self, key, (value, self.frame)) 72 | 73 | def __repr__(self): 74 | """ Printable version of the dict instead of getting memory adress """ 75 | print_list = [] 76 | with self.lock: 77 | for key, value in OrderedDict.items(self): 78 | if self.frame - value[1] < self.max_age: 79 | print_list.append(f"{repr(key)}: {repr(value)}") 80 | print_str = ", ".join(print_list) 81 | return f"ExpiringDict({print_str})" 82 | 83 | def __str__(self): 84 | return self.__repr__() 85 | 86 | def __iter__(self): 87 | """ Override 'for key in dict:' """ 88 | with self.lock: 89 | return self.keys() 90 | 91 | # TODO find a way to improve len 92 | def __len__(self): 93 | """Override len method as key value pairs aren't instantly being deleted, but only on __get__(item). 94 | This function is slow because it has to check if each element is not expired yet.""" 95 | with self.lock: 96 | count = 0 97 | for _ in self.values(): 98 | count += 1 99 | return count 100 | 101 | def pop(self, key, default=None, with_age=False): 102 | """ Return the item and remove it """ 103 | with self.lock: 104 | if OrderedDict.__contains__(self, key): 105 | item = OrderedDict.__getitem__(self, key) 106 | if self.frame - item[1] < self.max_age: 107 | del self[key] 108 | if with_age: 109 | return item[0], item[1] 110 | return item[0] 111 | del self[key] 112 | if default is None: 113 | raise KeyError(key) 114 | if with_age: 115 | return default, self.frame 116 | return default 117 | 118 | def get(self, key, default=None, with_age=False): 119 | """ Return the value for key if key is in dict, else default """ 120 | with self.lock: 121 | if OrderedDict.__contains__(self, key): 122 | item = OrderedDict.__getitem__(self, key) 123 | if self.frame - item[1] < self.max_age: 124 | if with_age: 125 | return item[0], item[1] 126 | return item[0] 127 | if default is None: 128 | raise KeyError(key) 129 | if with_age: 130 | return default, self.frame 131 | return None 132 | return None 133 | 134 | def update(self, other_dict: dict): 135 | with self.lock: 136 | for key, value in other_dict.items(): 137 | self[key] = value 138 | 139 | def items(self) -> Iterable: 140 | """ Return iterator of zipped list [keys, values] """ 141 | with self.lock: 142 | for key, value in OrderedDict.items(self): 143 | if self.frame - value[1] < self.max_age: 144 | yield key, value[0] 145 | 146 | def keys(self) -> Iterable: 147 | """ Return iterator of keys """ 148 | with self.lock: 149 | for key, value in OrderedDict.items(self): 150 | if self.frame - value[1] < self.max_age: 151 | yield key 152 | 153 | def values(self) -> Iterable: 154 | """ Return iterator of values """ 155 | with self.lock: 156 | for value in OrderedDict.values(self): 157 | if self.frame - value[1] < self.max_age: 158 | yield value[0] 159 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/generate_ids.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0212 2 | import importlib 3 | import json 4 | import platform 5 | import subprocess 6 | import sys 7 | from pathlib import Path 8 | 9 | from loguru import logger 10 | 11 | from sc2.game_data import AbilityData, GameData, UnitTypeData, UpgradeData 12 | from sc2.ids.ability_id import AbilityId 13 | 14 | try: 15 | from sc2.ids.id_version import ID_VERSION_STRING 16 | except ImportError: 17 | ID_VERSION_STRING = "4.11.4.78285" 18 | 19 | 20 | class IdGenerator: 21 | 22 | def __init__(self, game_data: GameData = None, game_version: str = None, verbose: bool = False): 23 | self.game_data: GameData = game_data 24 | self.game_version = game_version 25 | self.verbose = verbose 26 | 27 | self.HEADER = f'# DO NOT EDIT!\n# This file was automatically generated by "{Path(__file__).name}"\n' 28 | 29 | self.PF = platform.system() 30 | 31 | self.HOME_DIR = str(Path.home()) 32 | self.DATA_JSON = { 33 | "Darwin": self.HOME_DIR + "/Library/Application Support/Blizzard/StarCraft II/stableid.json", 34 | "Windows": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", 35 | "Linux": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", 36 | } 37 | 38 | self.ENUM_TRANSLATE = { 39 | "Units": "UnitTypeId", 40 | "Abilities": "AbilityId", 41 | "Upgrades": "UpgradeId", 42 | "Buffs": "BuffId", 43 | "Effects": "EffectId", 44 | } 45 | 46 | self.FILE_TRANSLATE = { 47 | "Units": "unit_typeid", 48 | "Abilities": "ability_id", 49 | "Upgrades": "upgrade_id", 50 | "Buffs": "buff_id", 51 | "Effects": "effect_id", 52 | } 53 | 54 | @staticmethod 55 | def make_key(key): 56 | if key[0].isdigit(): 57 | key = "_" + key 58 | # In patch 5.0, the key has "@" character in it which is not possible with python enums 59 | return key.upper().replace(" ", "_").replace("@", "") 60 | 61 | def parse_data(self, data): 62 | # for d in data: # Units, Abilities, Upgrades, Buffs, Effects 63 | 64 | units = self.parse_simple("Units", data) 65 | upgrades = self.parse_simple("Upgrades", data) 66 | effects = self.parse_simple("Effects", data) 67 | buffs = self.parse_simple("Buffs", data) 68 | 69 | abilities = {} 70 | for v in data["Abilities"]: 71 | key = v["buttonname"] 72 | remapid = v.get("remapid") 73 | 74 | if (not key) and (remapid is None): 75 | assert v["buttonname"] == "" 76 | continue 77 | 78 | if not key: 79 | if v["friendlyname"] != "": 80 | key = v["friendlyname"] 81 | else: 82 | sys.exit(f"Not mapped: {v !r}") 83 | 84 | key = key.upper().replace(" ", "_").replace("@", "") 85 | 86 | if "name" in v: 87 | key = f'{v["name"].upper().replace(" ", "_")}_{key}' 88 | 89 | if "friendlyname" in v: 90 | key = v["friendlyname"].upper().replace(" ", "_") 91 | 92 | if key[0].isdigit(): 93 | key = "_" + key 94 | 95 | if key in abilities and v["index"] == 0: 96 | logger.info(f"{key} has value 0 and id {v['id']}, overwriting {key}: {abilities[key]}") 97 | # Commented out to try to fix: 3670 is not a valid AbilityId 98 | abilities[key] = v["id"] 99 | elif key in abilities: 100 | logger.info(f"{key} has appeared a second time with id={v['id']}") 101 | else: 102 | abilities[key] = v["id"] 103 | 104 | abilities["SMART"] = 1 105 | 106 | enums = {} 107 | enums["Units"] = units 108 | enums["Abilities"] = abilities 109 | enums["Upgrades"] = upgrades 110 | enums["Buffs"] = buffs 111 | enums["Effects"] = effects 112 | 113 | return enums 114 | 115 | def parse_simple(self, d, data): 116 | units = {} 117 | for v in data[d]: 118 | key = v["name"] 119 | 120 | if not key: 121 | continue 122 | key_to_insert = self.make_key(key) 123 | if key_to_insert in units: 124 | index = 2 125 | tmp = f"{key_to_insert}_{index}" 126 | while tmp in units: 127 | index += 1 128 | tmp = f"{key_to_insert}_{index}" 129 | key_to_insert = tmp 130 | units[key_to_insert] = v["id"] 131 | 132 | return units 133 | 134 | def generate_python_code(self, enums): 135 | assert {"Units", "Abilities", "Upgrades", "Buffs", "Effects"} <= enums.keys() 136 | 137 | sc2dir = Path(__file__).parent 138 | idsdir = sc2dir / "ids" 139 | idsdir.mkdir(exist_ok=True) 140 | 141 | with (idsdir / "__init__.py").open("w") as f: 142 | initstring = f"__all__ = {[n.lower() for n in self.FILE_TRANSLATE.values()] !r}\n".replace("'", '"') 143 | f.write("\n".join([self.HEADER, initstring])) 144 | 145 | for name, body in enums.items(): 146 | class_name = self.ENUM_TRANSLATE[name] 147 | 148 | code = [self.HEADER, "import enum", "\n", f"class {class_name}(enum.Enum):"] 149 | 150 | for key, value in sorted(body.items(), key=lambda p: p[1]): 151 | code.append(f" {key} = {value}") 152 | 153 | # Add repr function to more easily dump enums to dict 154 | code += f""" 155 | def __repr__(self) -> str: 156 | return f"{class_name}.{{self.name}}" 157 | """.split("\n") 158 | 159 | # Add missing ids function to not make the game crash when unknown BuffId was detected 160 | if class_name == "BuffId": 161 | code += f""" 162 | @classmethod 163 | def _missing_(cls, value: int) -> "{class_name}": 164 | return cls.NULL 165 | """.split("\n") 166 | 167 | code += f""" 168 | for item in {class_name}: 169 | globals()[item.name] = item 170 | """.split("\n") 171 | 172 | ids_file_path = (idsdir / self.FILE_TRANSLATE[name]).with_suffix(".py") 173 | with ids_file_path.open("w") as f: 174 | f.write("\n".join(code)) 175 | 176 | # Apply formatting 177 | try: 178 | subprocess.run(["poetry", "run", "yapf", ids_file_path, "-i"], check=True) 179 | except FileNotFoundError: 180 | logger.info( 181 | f"Yapf is not installed. Please use 'pip install yapf' to install yapf formatter.\nCould not autoformat file {ids_file_path}" 182 | ) 183 | 184 | if self.game_version is not None: 185 | version_path = Path(__file__).parent / "ids" / "id_version.py" 186 | with open(version_path, "w") as f: 187 | f.write(f'ID_VERSION_STRING = "{self.game_version}"\n') 188 | 189 | def update_ids_from_stableid_json(self): 190 | if self.game_version is None or ID_VERSION_STRING is None or ID_VERSION_STRING != self.game_version: 191 | if self.verbose and self.game_version is not None and ID_VERSION_STRING is not None: 192 | logger.info( 193 | f"Game version is different (Old: {self.game_version}, new: {ID_VERSION_STRING}. Updating ids to match game version" 194 | ) 195 | stable_id_path = Path(self.DATA_JSON[self.PF]) 196 | assert stable_id_path.is_file(), f"stable_id.json was not found at path \"{stable_id_path}\"" 197 | with stable_id_path.open(encoding="utf-8") as data_file: 198 | data = json.loads(data_file.read()) 199 | self.generate_python_code(self.parse_data(data)) 200 | 201 | # Update game_data if this is a live game 202 | if self.game_data is not None: 203 | self.reimport_ids() 204 | self.update_game_data() 205 | 206 | @staticmethod 207 | def reimport_ids(): 208 | 209 | # Reload the newly written "id" files 210 | # TODO This only re-imports modules, but if they haven't been imported, it will yield an error 211 | importlib.reload(sys.modules["sc2.ids.ability_id"]) 212 | 213 | importlib.reload(sys.modules["sc2.ids.unit_typeid"]) 214 | 215 | importlib.reload(sys.modules["sc2.ids.upgrade_id"]) 216 | 217 | importlib.reload(sys.modules["sc2.ids.effect_id"]) 218 | 219 | importlib.reload(sys.modules["sc2.ids.buff_id"]) 220 | 221 | # importlib.reload(sys.modules["sc2.ids.id_version"]) 222 | 223 | importlib.reload(sys.modules["sc2.constants"]) 224 | 225 | def update_game_data(self): 226 | """Re-generate the dicts from self.game_data. 227 | This should be done after the ids have been reimported.""" 228 | ids = set(a.value for a in AbilityId if a.value != 0) 229 | self.game_data.abilities = { 230 | a.ability_id: AbilityData(self.game_data, a) 231 | for a in self.game_data._proto.abilities if a.ability_id in ids 232 | } 233 | # self.game_data.abilities = { 234 | # a.ability_id: AbilityData(self.game_data, a) for a in self.game_data._proto.abilities 235 | # } 236 | self.game_data.units = { 237 | u.unit_id: UnitTypeData(self.game_data, u) 238 | for u in self.game_data._proto.units if u.available 239 | } 240 | self.game_data.upgrades = {u.upgrade_id: UpgradeData(self.game_data, u) for u in self.game_data._proto.upgrades} 241 | 242 | 243 | if __name__ == "__main__": 244 | updater = IdGenerator() 245 | updater.update_ids_from_stableid_json() 246 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/ids/__init__.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_ids.py" 3 | 4 | __all__ = ["unit_typeid", "ability_id", "upgrade_id", "buff_id", "effect_id"] 5 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/ids/effect_id.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_ids.py" 3 | 4 | import enum 5 | 6 | 7 | class EffectId(enum.Enum): 8 | NULL = 0 9 | PSISTORMPERSISTENT = 1 10 | GUARDIANSHIELDPERSISTENT = 2 11 | TEMPORALFIELDGROWINGBUBBLECREATEPERSISTENT = 3 12 | TEMPORALFIELDAFTERBUBBLECREATEPERSISTENT = 4 13 | THERMALLANCESFORWARD = 5 14 | SCANNERSWEEP = 6 15 | NUKEPERSISTENT = 7 16 | LIBERATORTARGETMORPHDELAYPERSISTENT = 8 17 | LIBERATORTARGETMORPHPERSISTENT = 9 18 | BLINDINGCLOUDCP = 10 19 | RAVAGERCORROSIVEBILECP = 11 20 | LURKERMP = 12 21 | 22 | def __repr__(self) -> str: 23 | return f"EffectId.{self.name}" 24 | 25 | 26 | for item in EffectId: 27 | globals()[item.name] = item 28 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/ids/id_version.py: -------------------------------------------------------------------------------- 1 | ID_VERSION_STRING = "4.11.4.78285" 2 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/maps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from loguru import logger 6 | 7 | from sc2.paths import Paths 8 | 9 | 10 | def get(name: str) -> Map: 11 | # Iterate through 2 folder depths 12 | for map_dir in (p for p in Paths.MAPS.iterdir()): 13 | if map_dir.is_dir(): 14 | for map_file in (p for p in map_dir.iterdir()): 15 | if Map.matches_target_map_name(map_file, name): 16 | return Map(map_file) 17 | elif Map.matches_target_map_name(map_dir, name): 18 | return Map(map_dir) 19 | 20 | raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".") 21 | 22 | 23 | class Map: 24 | 25 | def __init__(self, path: Path): 26 | self.path = path 27 | 28 | if self.path.is_absolute(): 29 | try: 30 | self.relative_path = self.path.relative_to(Paths.MAPS) 31 | except ValueError: # path not relative to basedir 32 | logger.warning(f"Using absolute path: {self.path}") 33 | self.relative_path = self.path 34 | else: 35 | self.relative_path = self.path 36 | 37 | @property 38 | def name(self): 39 | return self.path.stem 40 | 41 | @property 42 | def data(self): 43 | with open(self.path, "rb") as f: 44 | return f.read() 45 | 46 | def __repr__(self): 47 | return f"Map({self.path})" 48 | 49 | @classmethod 50 | def is_map_file(cls, file: Path) -> bool: 51 | return file.is_file() and file.suffix == ".SC2Map" 52 | 53 | @classmethod 54 | def matches_target_map_name(cls, file: Path, name: str) -> bool: 55 | return cls.is_map_file(file) and file.stem == name 56 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/observer_ai.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class is very experimental and probably not up to date and needs to be refurbished. 3 | If it works, you can watch replays with it. 4 | """ 5 | 6 | # pylint: disable=W0201,W0212 7 | from __future__ import annotations 8 | 9 | from typing import TYPE_CHECKING, List, Union 10 | 11 | from sc2.bot_ai_internal import BotAIInternal 12 | from sc2.data import Alert, Result 13 | from sc2.game_data import GameData 14 | from sc2.ids.ability_id import AbilityId 15 | from sc2.ids.upgrade_id import UpgradeId 16 | from sc2.position import Point2 17 | from sc2.unit import Unit 18 | from sc2.units import Units 19 | 20 | if TYPE_CHECKING: 21 | from sc2.client import Client 22 | from sc2.game_info import GameInfo 23 | 24 | 25 | class ObserverAI(BotAIInternal): 26 | """Base class for bots.""" 27 | 28 | @property 29 | def time(self) -> float: 30 | """ Returns time in seconds, assumes the game is played on 'faster' """ 31 | return self.state.game_loop / 22.4 # / (1/1.4) * (1/16) 32 | 33 | @property 34 | def time_formatted(self) -> str: 35 | """ Returns time as string in min:sec format """ 36 | t = self.time 37 | return f"{int(t // 60):02}:{int(t % 60):02}" 38 | 39 | @property 40 | def game_info(self) -> GameInfo: 41 | """ See game_info.py """ 42 | return self._game_info 43 | 44 | @property 45 | def game_data(self) -> GameData: 46 | """ See game_data.py """ 47 | return self._game_data 48 | 49 | @property 50 | def client(self) -> Client: 51 | """ See client.py """ 52 | return self._client 53 | 54 | def alert(self, alert_code: Alert) -> bool: 55 | """ 56 | Check if alert is triggered in the current step. 57 | Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702 58 | 59 | Example use: 60 | 61 | from sc2.data import Alert 62 | if self.alert(Alert.AddOnComplete): 63 | print("Addon Complete") 64 | 65 | Alert codes:: 66 | 67 | AlertError 68 | AddOnComplete 69 | BuildingComplete 70 | BuildingUnderAttack 71 | LarvaHatched 72 | MergeComplete 73 | MineralsExhausted 74 | MorphComplete 75 | MothershipComplete 76 | MULEExpired 77 | NuclearLaunchDetected 78 | NukeComplete 79 | NydusWormDetected 80 | ResearchComplete 81 | TrainError 82 | TrainUnitComplete 83 | TrainWorkerComplete 84 | TransformationComplete 85 | UnitUnderAttack 86 | UpgradeComplete 87 | VespeneExhausted 88 | WarpInComplete 89 | 90 | :param alert_code: 91 | """ 92 | assert isinstance(alert_code, Alert), f"alert_code {alert_code} is no Alert" 93 | return alert_code.value in self.state.alerts 94 | 95 | @property 96 | def start_location(self) -> Point2: 97 | """ 98 | Returns the spawn location of the bot, using the position of the first created townhall. 99 | This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start. 100 | """ 101 | return self.game_info.player_start_location 102 | 103 | @property 104 | def enemy_start_locations(self) -> List[Point2]: 105 | """Possible start locations for enemies.""" 106 | return self.game_info.start_locations 107 | 108 | async def get_available_abilities( 109 | self, units: Union[List[Unit], Units], ignore_resource_requirements: bool = False 110 | ) -> List[List[AbilityId]]: 111 | """Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched. 112 | 113 | Examples:: 114 | 115 | units_abilities = await self.get_available_abilities(self.units) 116 | 117 | or:: 118 | 119 | units_abilities = await self.get_available_abilities([self.units.random]) 120 | 121 | :param units: 122 | :param ignore_resource_requirements:""" 123 | return await self.client.query_available_abilities(units, ignore_resource_requirements) 124 | 125 | async def on_unit_destroyed(self, unit_tag: int): 126 | """ 127 | Override this in your bot class. 128 | This will event will be called when a unit (or structure, friendly or enemy) dies. 129 | For enemy units, this only works if the enemy unit was in vision on death. 130 | 131 | :param unit_tag: 132 | """ 133 | 134 | async def on_unit_created(self, unit: Unit): 135 | """Override this in your bot class. This function is called when a unit is created. 136 | 137 | :param unit:""" 138 | 139 | async def on_building_construction_started(self, unit: Unit): 140 | """ 141 | Override this in your bot class. 142 | This function is called when a building construction has started. 143 | 144 | :param unit: 145 | """ 146 | 147 | async def on_building_construction_complete(self, unit: Unit): 148 | """ 149 | Override this in your bot class. This function is called when a building 150 | construction is completed. 151 | 152 | :param unit: 153 | """ 154 | 155 | async def on_upgrade_complete(self, upgrade: UpgradeId): 156 | """ 157 | Override this in your bot class. This function is called with the upgrade id of an upgrade that was not finished last step and is now. 158 | 159 | :param upgrade: 160 | """ 161 | 162 | async def on_start(self): 163 | """ 164 | Override this in your bot class. This function is called after "on_start". 165 | At this point, game_data, game_info and the first iteration of game_state (self.state) are available. 166 | """ 167 | 168 | async def on_step(self, iteration: int): 169 | """ 170 | You need to implement this function! 171 | Override this in your bot class. 172 | This function is called on every game step (looped in realtime mode). 173 | 174 | :param iteration: 175 | """ 176 | raise NotImplementedError 177 | 178 | async def on_end(self, game_result: Result): 179 | """Override this in your bot class. This function is called at the end of a game. 180 | 181 | :param game_result:""" 182 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import re 4 | import sys 5 | from contextlib import suppress 6 | from pathlib import Path 7 | 8 | from loguru import logger 9 | 10 | from sc2 import wsl 11 | 12 | BASEDIR = { 13 | "Windows": "C:/Program Files (x86)/StarCraft II", 14 | "WSL1": "/mnt/c/Program Files (x86)/StarCraft II", 15 | "WSL2": "/mnt/c/Program Files (x86)/StarCraft II", 16 | "Darwin": "/Applications/StarCraft II", 17 | "Linux": "~/StarCraftII", 18 | "WineLinux": "~/.wine/drive_c/Program Files (x86)/StarCraft II", 19 | } 20 | 21 | USERPATH = { 22 | "Windows": "Documents\\StarCraft II\\ExecuteInfo.txt", 23 | "WSL1": "Documents/StarCraft II/ExecuteInfo.txt", 24 | "WSL2": "Documents/StarCraft II/ExecuteInfo.txt", 25 | "Darwin": "Library/Application Support/Blizzard/StarCraft II/ExecuteInfo.txt", 26 | "Linux": None, 27 | "WineLinux": None, 28 | } 29 | 30 | BINPATH = { 31 | "Windows": "SC2_x64.exe", 32 | "WSL1": "SC2_x64.exe", 33 | "WSL2": "SC2_x64.exe", 34 | "Darwin": "SC2.app/Contents/MacOS/SC2", 35 | "Linux": "SC2_x64", 36 | "WineLinux": "SC2_x64.exe", 37 | } 38 | 39 | CWD = { 40 | "Windows": "Support64", 41 | "WSL1": "Support64", 42 | "WSL2": "Support64", 43 | "Darwin": None, 44 | "Linux": None, 45 | "WineLinux": "Support64", 46 | } 47 | 48 | 49 | def platform_detect(): 50 | pf = os.environ.get("SC2PF", platform.system()) 51 | if pf == "Linux": 52 | return wsl.detect() or pf 53 | return pf 54 | 55 | 56 | PF = platform_detect() 57 | 58 | 59 | def get_home(): 60 | """Get home directory of user, using Windows home directory for WSL.""" 61 | if PF in {"WSL1", "WSL2"}: 62 | return wsl.get_wsl_home() or Path.home().expanduser() 63 | return Path.home().expanduser() 64 | 65 | 66 | def get_user_sc2_install(): 67 | """Attempts to find a user's SC2 install if their OS has ExecuteInfo.txt""" 68 | if USERPATH[PF]: 69 | einfo = str(get_home() / Path(USERPATH[PF])) 70 | if os.path.isfile(einfo): 71 | with open(einfo) as f: 72 | content = f.read() 73 | if content: 74 | base = re.search(r" = (.*)Versions", content).group(1) 75 | if PF in {"WSL1", "WSL2"}: 76 | base = str(wsl.win_path_to_wsl_path(base)) 77 | 78 | if os.path.exists(base): 79 | return base 80 | return None 81 | 82 | 83 | def get_env(): 84 | # TODO: Linux env conf from: https://github.com/deepmind/pysc2/blob/master/pysc2/run_configs/platforms.py 85 | return None 86 | 87 | 88 | def get_runner_args(cwd): 89 | if "WINE" in os.environ: 90 | runner_file = Path(os.environ.get("WINE")) 91 | runner_file = runner_file if runner_file.is_file() else runner_file / "wine" 92 | """ 93 | TODO Is converting linux path really necessary? 94 | That would convert 95 | '/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/Support64' 96 | to 97 | 'Z:\\home\\burny\\Games\\battlenet\\drive_c\\Program Files (x86)\\StarCraft II\\Support64' 98 | """ 99 | return [runner_file, "start", "/d", cwd, "/unix"] 100 | return [] 101 | 102 | 103 | def latest_executeble(versions_dir, base_build=None): 104 | latest = None 105 | 106 | if base_build is not None: 107 | with suppress(ValueError): 108 | latest = ( 109 | int(base_build[4:]), 110 | max(p for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith(str(base_build))), 111 | ) 112 | 113 | if base_build is None or latest is None: 114 | latest = max((int(p.name[4:]), p) for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith("Base")) 115 | 116 | version, path = latest 117 | 118 | if version < 55958: 119 | logger.critical("Your SC2 binary is too old. Upgrade to 3.16.1 or newer.") 120 | sys.exit(1) 121 | return path / BINPATH[PF] 122 | 123 | 124 | class _MetaPaths(type): 125 | """"Lazily loads paths to allow importing the library even if SC2 isn't installed.""" 126 | 127 | # pylint: disable=C0203 128 | def __setup(self): 129 | if PF not in BASEDIR: 130 | logger.critical(f"Unsupported platform '{PF}'") 131 | sys.exit(1) 132 | 133 | try: 134 | base = os.environ.get("SC2PATH") or get_user_sc2_install() or BASEDIR[PF] 135 | self.BASE = Path(base).expanduser() 136 | self.EXECUTABLE = latest_executeble(self.BASE / "Versions") 137 | self.CWD = self.BASE / CWD[PF] if CWD[PF] else None 138 | 139 | self.REPLAYS = self.BASE / "Replays" 140 | 141 | if (self.BASE / "maps").exists(): 142 | self.MAPS = self.BASE / "maps" 143 | else: 144 | self.MAPS = self.BASE / "Maps" 145 | except FileNotFoundError as e: 146 | logger.critical(f"SC2 installation not found: File '{e.filename}' does not exist.") 147 | sys.exit(1) 148 | 149 | # pylint: disable=C0203 150 | def __getattr__(self, attr): 151 | # pylint: disable=E1120 152 | self.__setup() 153 | return getattr(self, attr) 154 | 155 | 156 | class Paths(metaclass=_MetaPaths): 157 | """Paths for SC2 folders, lazily loaded using the above metaclass.""" 158 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/pixel_map.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Callable, FrozenSet, List, Set, Tuple, Union 3 | 4 | import numpy as np 5 | 6 | from sc2.position import Point2 7 | 8 | 9 | class PixelMap: 10 | 11 | def __init__(self, proto, in_bits: bool = False): 12 | """ 13 | :param proto: 14 | :param in_bits: 15 | """ 16 | self._proto = proto 17 | # Used for copying pixelmaps 18 | self._in_bits: bool = in_bits 19 | 20 | assert self.width * self.height == (8 if in_bits else 1) * len( 21 | self._proto.data 22 | ), f"{self.width * self.height} {(8 if in_bits else 1)*len(self._proto.data)}" 23 | buffer_data = np.frombuffer(self._proto.data, dtype=np.uint8) 24 | if in_bits: 25 | buffer_data = np.unpackbits(buffer_data) 26 | self.data_numpy = buffer_data.reshape(self._proto.size.y, self._proto.size.x) 27 | 28 | @property 29 | def width(self) -> int: 30 | return self._proto.size.x 31 | 32 | @property 33 | def height(self) -> int: 34 | return self._proto.size.y 35 | 36 | @property 37 | def bits_per_pixel(self) -> int: 38 | return self._proto.bits_per_pixel 39 | 40 | @property 41 | def bytes_per_pixel(self) -> int: 42 | return self._proto.bits_per_pixel // 8 43 | 44 | def __getitem__(self, pos: Tuple[int, int]) -> int: 45 | """ Example usage: is_pathable = self._game_info.pathing_grid[Point2((20, 20))] != 0 """ 46 | assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" 47 | assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" 48 | return int(self.data_numpy[pos[1], pos[0]]) 49 | 50 | def __setitem__(self, pos: Tuple[int, int], value: int): 51 | """ Example usage: self._game_info.pathing_grid[Point2((20, 20))] = 255 """ 52 | assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" 53 | assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" 54 | assert ( 55 | 0 <= value <= 254 * self._in_bits + 1 56 | ), f"value is {value}, it should be between 0 and {254 * self._in_bits + 1}" 57 | assert isinstance(value, int), f"value is of type {type(value)}, it should be an integer" 58 | self.data_numpy[pos[1], pos[0]] = value 59 | 60 | def is_set(self, p: Tuple[int, int]) -> bool: 61 | return self[p] != 0 62 | 63 | def is_empty(self, p: Tuple[int, int]) -> bool: 64 | return not self.is_set(p) 65 | 66 | def copy(self) -> "PixelMap": 67 | return PixelMap(self._proto, in_bits=self._in_bits) 68 | 69 | def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> Set[Point2]: 70 | nodes: Set[Point2] = set() 71 | queue: List[Point2] = [start_point] 72 | 73 | while queue: 74 | x, y = queue.pop() 75 | 76 | if not (0 <= x < self.width and 0 <= y < self.height): 77 | continue 78 | 79 | if Point2((x, y)) in nodes: 80 | continue 81 | 82 | if pred(self[x, y]): 83 | nodes.add(Point2((x, y))) 84 | queue += [Point2((x + a, y + b)) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)] 85 | return nodes 86 | 87 | def flood_fill_all(self, pred: Callable[[int], bool]) -> Set[FrozenSet[Point2]]: 88 | groups: Set[FrozenSet[Point2]] = set() 89 | 90 | for x in range(self.width): 91 | for y in range(self.height): 92 | if any((x, y) in g for g in groups): 93 | continue 94 | 95 | if pred(self[x, y]): 96 | groups.add(frozenset(self.flood_fill(Point2((x, y)), pred))) 97 | 98 | return groups 99 | 100 | def print(self, wide: bool = False) -> None: 101 | for y in range(self.height): 102 | for x in range(self.width): 103 | print("#" if self.is_set((x, y)) else " ", end=(" " if wide else "")) 104 | print("") 105 | 106 | def save_image(self, filename: Union[str, Path]): 107 | data = [(0, 0, self[x, y]) for y in range(self.height) for x in range(self.width)] 108 | # pylint: disable=C0415 109 | from PIL import Image 110 | 111 | im = Image.new("RGB", (self.width, self.height)) 112 | im.putdata(data) # type: ignore 113 | im.save(filename) 114 | 115 | def plot(self): 116 | # pylint: disable=C0415 117 | import matplotlib.pyplot as plt 118 | 119 | plt.imshow(self.data_numpy, origin="lower") 120 | plt.show() 121 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/player.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from pathlib import Path 3 | from typing import List, Union 4 | 5 | from sc2.bot_ai import BotAI 6 | from sc2.data import AIBuild, Difficulty, PlayerType, Race 7 | 8 | 9 | class AbstractPlayer(ABC): 10 | 11 | def __init__( 12 | self, 13 | p_type: PlayerType, 14 | race: Race = None, 15 | name: str = None, 16 | difficulty=None, 17 | ai_build=None, 18 | fullscreen=False 19 | ): 20 | assert isinstance(p_type, PlayerType), f"p_type is of type {type(p_type)}" 21 | assert name is None or isinstance(name, str), f"name is of type {type(name)}" 22 | 23 | self.name = name 24 | self.type = p_type 25 | self.fullscreen = fullscreen 26 | if race is not None: 27 | self.race = race 28 | if p_type == PlayerType.Computer: 29 | assert isinstance(difficulty, Difficulty), f"difficulty is of type {type(difficulty)}" 30 | # Workaround, proto information does not carry ai_build info 31 | # We cant set that in the Player classmethod 32 | assert ai_build is None or isinstance(ai_build, AIBuild), f"ai_build is of type {type(ai_build)}" 33 | self.difficulty = difficulty 34 | self.ai_build = ai_build 35 | 36 | elif p_type == PlayerType.Observer: 37 | assert race is None 38 | assert difficulty is None 39 | assert ai_build is None 40 | 41 | else: 42 | assert isinstance(race, Race), f"race is of type {type(race)}" 43 | assert difficulty is None 44 | assert ai_build is None 45 | 46 | @property 47 | def needs_sc2(self): 48 | return not isinstance(self, Computer) 49 | 50 | 51 | class Human(AbstractPlayer): 52 | 53 | def __init__(self, race, name=None, fullscreen=False): 54 | super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) 55 | 56 | def __str__(self): 57 | if self.name is not None: 58 | return f"Human({self.race._name_}, name={self.name !r})" 59 | return f"Human({self.race._name_})" 60 | 61 | 62 | class Bot(AbstractPlayer): 63 | 64 | def __init__(self, race, ai, name=None, fullscreen=False): 65 | """ 66 | AI can be None if this player object is just used to inform the 67 | server about player types. 68 | """ 69 | assert isinstance(ai, BotAI) or ai is None, f"ai is of type {type(ai)}, inherit BotAI from bot_ai.py" 70 | super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) 71 | self.ai = ai 72 | 73 | def __str__(self): 74 | if self.name is not None: 75 | return f"Bot {self.ai.__class__.__name__}({self.race._name_}), name={self.name !r})" 76 | return f"Bot {self.ai.__class__.__name__}({self.race._name_})" 77 | 78 | 79 | class Computer(AbstractPlayer): 80 | 81 | def __init__(self, race, difficulty=Difficulty.Easy, ai_build=AIBuild.RandomBuild): 82 | super().__init__(PlayerType.Computer, race, difficulty=difficulty, ai_build=ai_build) 83 | 84 | def __str__(self): 85 | return f"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})" 86 | 87 | 88 | class Observer(AbstractPlayer): 89 | 90 | def __init__(self): 91 | super().__init__(PlayerType.Observer) 92 | 93 | def __str__(self): 94 | return "Observer" 95 | 96 | 97 | class Player(AbstractPlayer): 98 | 99 | def __init__(self, player_id, p_type, requested_race, difficulty=None, actual_race=None, name=None, ai_build=None): 100 | super().__init__(p_type, requested_race, difficulty=difficulty, name=name, ai_build=ai_build) 101 | self.id: int = player_id 102 | self.actual_race: Race = actual_race 103 | 104 | @classmethod 105 | def from_proto(cls, proto): 106 | if PlayerType(proto.type) == PlayerType.Observer: 107 | return cls(proto.player_id, PlayerType(proto.type), None, None, None) 108 | return cls( 109 | proto.player_id, 110 | PlayerType(proto.type), 111 | Race(proto.race_requested), 112 | Difficulty(proto.difficulty) if proto.HasField("difficulty") else None, 113 | Race(proto.race_actual) if proto.HasField("race_actual") else None, 114 | proto.player_name if proto.HasField("player_name") else None, 115 | ) 116 | 117 | 118 | class BotProcess(AbstractPlayer): 119 | """ 120 | Class for handling bots launched externally, including non-python bots. 121 | Default parameters comply with sc2ai and aiarena ladders. 122 | 123 | :param path: the executable file's path 124 | :param launch_list: list of strings that launches the bot e.g. ["python", "run.py"] or ["run.exe"] 125 | :param race: bot's race 126 | :param name: bot's name 127 | :param sc2port_arg: the accepted argument name for the port of the sc2 instance to listen to 128 | :param hostaddress_arg: the accepted argument name for the address of the sc2 instance to listen to 129 | :param match_arg: the accepted argument name for the starting port to generate a portconfig from 130 | :param realtime_arg: the accepted argument name for specifying realtime 131 | :param other_args: anything else that is needed 132 | 133 | e.g. to call a bot capable of running on the bot ladders: 134 | BotProcess(os.getcwd(), "python run.py", Race.Terran, "INnoVation") 135 | """ 136 | 137 | def __init__( 138 | self, 139 | path: Union[str, Path], 140 | launch_list: List[str], 141 | race: Race, 142 | name=None, 143 | sc2port_arg="--GamePort", 144 | hostaddress_arg="--LadderServer", 145 | match_arg="--StartPort", 146 | realtime_arg="--RealTime", 147 | other_args: str = None, 148 | stdout: str = None, 149 | ): 150 | super().__init__(PlayerType.Participant, race, name=name) 151 | assert Path(path).exists() 152 | self.path = path 153 | self.launch_list = launch_list 154 | self.sc2port_arg = sc2port_arg 155 | self.match_arg = match_arg 156 | self.hostaddress_arg = hostaddress_arg 157 | self.realtime_arg = realtime_arg 158 | self.other_args = other_args 159 | self.stdout = stdout 160 | 161 | def __repr__(self): 162 | if self.name is not None: 163 | return f"Bot {self.name}({self.race.name} from {self.launch_list})" 164 | return f"Bot({self.race.name} from {self.launch_list})" 165 | 166 | def cmd_line(self, 167 | sc2port: Union[int, str], 168 | matchport: Union[int, str], 169 | hostaddress: str, 170 | realtime: bool = False) -> List[str]: 171 | """ 172 | 173 | :param sc2port: the port that the launched sc2 instance listens to 174 | :param matchport: some starting port that both bots use to generate identical portconfigs. 175 | Note: This will not be sent if playing vs computer 176 | :param hostaddress: the address the sc2 instances used 177 | :param realtime: 1 or 0, indicating whether the match is played in realtime or not 178 | :return: string that will be used to start the bot's process 179 | """ 180 | cmd_line = [ 181 | *self.launch_list, 182 | self.sc2port_arg, 183 | str(sc2port), 184 | self.hostaddress_arg, 185 | hostaddress, 186 | ] 187 | if matchport is not None: 188 | cmd_line.extend([self.match_arg, str(matchport)]) 189 | if self.other_args is not None: 190 | cmd_line.append(self.other_args) 191 | if realtime: 192 | cmd_line.extend([self.realtime_arg]) 193 | return cmd_line 194 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/portconfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import portpicker 4 | 5 | 6 | class Portconfig: 7 | """ 8 | A data class for ports used by participants to join a match. 9 | 10 | EVERY participant joining the match must send the same sets of ports to join successfully. 11 | SC2 needs 2 ports per connection (one for data, one as a 'header'), which is why the ports come in pairs. 12 | 13 | :param guests: number of non-hosting participants in a match (i.e. 1 less than the number of participants) 14 | :param server_ports: [int portA, int portB] 15 | :param player_ports: [[int port1A, int port1B], [int port2A, int port2B], ... ] 16 | 17 | .shared is deprecated, and should TODO be removed soon (once ladderbots' __init__.py doesnt specify them). 18 | 19 | .server contains the pair of ports used by the participant 'hosting' the match 20 | 21 | .players contains a pair of ports for every 'guest' (non-hosting participants) in the match 22 | E.g. for 1v1, there will be only 1 guest. For 2v2 (coming soonTM), there would be 3 guests. 23 | """ 24 | 25 | def __init__(self, guests=1, server_ports=None, player_ports=None): 26 | self.shared = None 27 | self._picked_ports = [] 28 | if server_ports: 29 | self.server = server_ports 30 | else: 31 | self.server = [portpicker.pick_unused_port() for _ in range(2)] 32 | self._picked_ports.extend(self.server) 33 | if player_ports: 34 | self.players = player_ports 35 | else: 36 | self.players = [[portpicker.pick_unused_port() for _ in range(2)] for _ in range(guests)] 37 | self._picked_ports.extend(port for player in self.players for port in player) 38 | 39 | def clean(self): 40 | while self._picked_ports: 41 | portpicker.return_port(self._picked_ports.pop()) 42 | 43 | def __str__(self): 44 | return f"Portconfig(shared={self.shared}, server={self.server}, players={self.players})" 45 | 46 | @property 47 | def as_json(self): 48 | return json.dumps({"shared": self.shared, "server": self.server, "players": self.players}) 49 | 50 | @classmethod 51 | def contiguous_ports(cls, guests=1, attempts=40): 52 | """Returns a Portconfig with adjacent ports""" 53 | for _ in range(attempts): 54 | start = portpicker.pick_unused_port() 55 | others = [start + j for j in range(1, 2 + guests * 2)] 56 | if all(portpicker.is_port_free(p) for p in others): 57 | server_ports = [start, others.pop(0)] 58 | player_ports = [] 59 | while others: 60 | player_ports.append([others.pop(0), others.pop(0)]) 61 | pc = cls(server_ports=server_ports, player_ports=player_ports) 62 | pc._picked_ports.append(start) 63 | return pc 64 | raise portpicker.NoFreePortFoundError() 65 | 66 | @classmethod 67 | def from_json(cls, json_data): 68 | data = json.loads(json_data) 69 | return cls(server_ports=data["server"], player_ports=data["players"]) 70 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/power_source.py: -------------------------------------------------------------------------------- 1 | from sc2.position import Point2 2 | 3 | 4 | class PowerSource: 5 | 6 | @classmethod 7 | def from_proto(cls, proto): 8 | return cls(Point2.from_proto(proto.pos), proto.radius, proto.tag) 9 | 10 | def __init__(self, position, radius, unit_tag): 11 | assert isinstance(position, Point2) 12 | assert radius > 0 13 | self.position = position 14 | self.radius = radius 15 | self.unit_tag = unit_tag 16 | 17 | def covers(self, position): 18 | return self.position.distance_to(position) <= self.radius 19 | 20 | def __repr__(self): 21 | return f"PowerSource({self.position}, {self.radius})" 22 | 23 | 24 | class PsionicMatrix: 25 | 26 | @classmethod 27 | def from_proto(cls, proto): 28 | return cls([PowerSource.from_proto(p) for p in proto]) 29 | 30 | def __init__(self, sources): 31 | self.sources = sources 32 | 33 | def covers(self, position): 34 | return any(source.covers(position) for source in self.sources) 35 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/protocol.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from contextlib import suppress 4 | 5 | from aiohttp import ClientWebSocketResponse 6 | from loguru import logger 7 | from s2clientprotocol import sc2api_pb2 as sc_pb 8 | 9 | from sc2.data import Status 10 | 11 | 12 | class ProtocolError(Exception): 13 | 14 | @property 15 | def is_game_over_error(self) -> bool: 16 | return self.args[0] in ["['Game has already ended']", "['Not supported if game has already ended']"] 17 | 18 | 19 | class ConnectionAlreadyClosed(ProtocolError): 20 | pass 21 | 22 | 23 | class Protocol: 24 | 25 | def __init__(self, ws): 26 | """ 27 | A class for communicating with an SCII application. 28 | :param ws: the websocket (type: aiohttp.ClientWebSocketResponse) used to communicate with a specific SCII app 29 | """ 30 | assert ws 31 | self._ws: ClientWebSocketResponse = ws 32 | self._status: Status = None 33 | 34 | async def __request(self, request): 35 | logger.debug(f"Sending request: {request !r}") 36 | try: 37 | await self._ws.send_bytes(request.SerializeToString()) 38 | except TypeError as exc: 39 | logger.exception("Cannot send: Connection already closed.") 40 | raise ConnectionAlreadyClosed("Connection already closed.") from exc 41 | logger.debug("Request sent") 42 | 43 | response = sc_pb.Response() 44 | try: 45 | response_bytes = await self._ws.receive_bytes() 46 | except TypeError as exc: 47 | if self._status == Status.ended: 48 | logger.info("Cannot receive: Game has already ended.") 49 | raise ConnectionAlreadyClosed("Game has already ended") from exc 50 | logger.error("Cannot receive: Connection already closed.") 51 | raise ConnectionAlreadyClosed("Connection already closed.") from exc 52 | except asyncio.CancelledError: 53 | # If request is sent, the response must be received before reraising cancel 54 | try: 55 | await self._ws.receive_bytes() 56 | except asyncio.CancelledError: 57 | logger.critical("Requests must not be cancelled multiple times") 58 | sys.exit(2) 59 | raise 60 | 61 | response.ParseFromString(response_bytes) 62 | logger.debug("Response received") 63 | return response 64 | 65 | async def _execute(self, **kwargs): 66 | assert len(kwargs) == 1, "Only one request allowed by the API" 67 | 68 | response = await self.__request(sc_pb.Request(**kwargs)) 69 | 70 | new_status = Status(response.status) 71 | if new_status != self._status: 72 | logger.info(f"Client status changed to {new_status} (was {self._status})") 73 | self._status = new_status 74 | 75 | if response.error: 76 | logger.debug(f"Response contained an error: {response.error}") 77 | raise ProtocolError(f"{response.error}") 78 | 79 | return response 80 | 81 | async def ping(self): 82 | result = await self._execute(ping=sc_pb.RequestPing()) 83 | return result 84 | 85 | async def quit(self): 86 | with suppress(ConnectionAlreadyClosed, ConnectionResetError): 87 | await self._execute(quit=sc_pb.RequestQuit()) 88 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/renderer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from s2clientprotocol import score_pb2 as score_pb 4 | 5 | from sc2.position import Point2 6 | 7 | 8 | class Renderer: 9 | 10 | def __init__(self, client, map_size, minimap_size): 11 | self._client = client 12 | 13 | self._window = None 14 | self._map_size = map_size 15 | self._map_image = None 16 | self._minimap_size = minimap_size 17 | self._minimap_image = None 18 | self._mouse_x, self._mouse_y = None, None 19 | self._text_supply = None 20 | self._text_vespene = None 21 | self._text_minerals = None 22 | self._text_score = None 23 | self._text_time = None 24 | 25 | async def render(self, observation): 26 | render_data = observation.observation.render_data 27 | 28 | map_size = render_data.map.size 29 | map_data = render_data.map.data 30 | minimap_size = render_data.minimap.size 31 | minimap_data = render_data.minimap.data 32 | 33 | map_width, map_height = map_size.x, map_size.y 34 | map_pitch = -map_width * 3 35 | 36 | minimap_width, minimap_height = minimap_size.x, minimap_size.y 37 | minimap_pitch = -minimap_width * 3 38 | 39 | if not self._window: 40 | # pylint: disable=C0415 41 | from pyglet.image import ImageData 42 | from pyglet.text import Label 43 | from pyglet.window import Window 44 | 45 | self._window = Window(width=map_width, height=map_height) 46 | self._window.on_mouse_press = self._on_mouse_press 47 | self._window.on_mouse_release = self._on_mouse_release 48 | self._window.on_mouse_drag = self._on_mouse_drag 49 | self._map_image = ImageData(map_width, map_height, "RGB", map_data, map_pitch) 50 | self._minimap_image = ImageData(minimap_width, minimap_height, "RGB", minimap_data, minimap_pitch) 51 | self._text_supply = Label( 52 | "", 53 | font_name="Arial", 54 | font_size=16, 55 | anchor_x="right", 56 | anchor_y="top", 57 | x=self._map_size[0] - 10, 58 | y=self._map_size[1] - 10, 59 | color=(200, 200, 200, 255), 60 | ) 61 | self._text_vespene = Label( 62 | "", 63 | font_name="Arial", 64 | font_size=16, 65 | anchor_x="right", 66 | anchor_y="top", 67 | x=self._map_size[0] - 130, 68 | y=self._map_size[1] - 10, 69 | color=(28, 160, 16, 255), 70 | ) 71 | self._text_minerals = Label( 72 | "", 73 | font_name="Arial", 74 | font_size=16, 75 | anchor_x="right", 76 | anchor_y="top", 77 | x=self._map_size[0] - 200, 78 | y=self._map_size[1] - 10, 79 | color=(68, 140, 255, 255), 80 | ) 81 | self._text_score = Label( 82 | "", 83 | font_name="Arial", 84 | font_size=16, 85 | anchor_x="left", 86 | anchor_y="top", 87 | x=10, 88 | y=self._map_size[1] - 10, 89 | color=(219, 30, 30, 255), 90 | ) 91 | self._text_time = Label( 92 | "", 93 | font_name="Arial", 94 | font_size=16, 95 | anchor_x="right", 96 | anchor_y="bottom", 97 | x=self._minimap_size[0] - 10, 98 | y=self._minimap_size[1] + 10, 99 | color=(255, 255, 255, 255), 100 | ) 101 | else: 102 | self._map_image.set_data("RGB", map_pitch, map_data) 103 | self._minimap_image.set_data("RGB", minimap_pitch, minimap_data) 104 | self._text_time.text = str(datetime.timedelta(seconds=(observation.observation.game_loop * 0.725) // 16)) 105 | if observation.observation.HasField("player_common"): 106 | self._text_supply.text = f"{observation.observation.player_common.food_used} / {observation.observation.player_common.food_cap}" 107 | self._text_vespene.text = str(observation.observation.player_common.vespene) 108 | self._text_minerals.text = str(observation.observation.player_common.minerals) 109 | if observation.observation.HasField("score"): 110 | # pylint: disable=W0212 111 | self._text_score.text = f"{score_pb._SCORE_SCORETYPE.values_by_number[observation.observation.score.score_type].name} score: {observation.observation.score.score}" 112 | 113 | await self._update_window() 114 | 115 | if self._client.in_game and (not observation.player_result) and self._mouse_x and self._mouse_y: 116 | await self._client.move_camera_spatial(Point2((self._mouse_x, self._minimap_size[0] - self._mouse_y))) 117 | self._mouse_x, self._mouse_y = None, None 118 | 119 | async def _update_window(self): 120 | self._window.switch_to() 121 | self._window.dispatch_events() 122 | 123 | self._window.clear() 124 | 125 | self._map_image.blit(0, 0) 126 | self._minimap_image.blit(0, 0) 127 | self._text_time.draw() 128 | self._text_score.draw() 129 | self._text_minerals.draw() 130 | self._text_vespene.draw() 131 | self._text_supply.draw() 132 | 133 | self._window.flip() 134 | 135 | def _on_mouse_press(self, x, y, button, _modifiers): 136 | if button != 1: # 1: mouse.LEFT 137 | return 138 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 139 | return 140 | self._mouse_x, self._mouse_y = x, y 141 | 142 | def _on_mouse_release(self, x, y, button, _modifiers): 143 | if button != 1: # 1: mouse.LEFT 144 | return 145 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 146 | return 147 | self._mouse_x, self._mouse_y = x, y 148 | 149 | def _on_mouse_drag(self, x, y, _dx, _dy, buttons, _modifiers): 150 | if not buttons & 1: # 1: mouse.LEFT 151 | return 152 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 153 | return 154 | self._mouse_x, self._mouse_y = x, y 155 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/sc2process.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import os.path 4 | import shutil 5 | import signal 6 | import subprocess 7 | import sys 8 | import tempfile 9 | import time 10 | from contextlib import suppress 11 | from typing import Any, Dict, List, Optional, Tuple, Union 12 | 13 | import aiohttp 14 | import portpicker 15 | from loguru import logger 16 | 17 | from sc2 import paths, wsl 18 | from sc2.controller import Controller 19 | from sc2.paths import Paths 20 | from sc2.versions import VERSIONS 21 | 22 | 23 | class kill_switch: 24 | _to_kill: List[Any] = [] 25 | 26 | @classmethod 27 | def add(cls, value): 28 | logger.debug("kill_switch: Add switch") 29 | cls._to_kill.append(value) 30 | 31 | @classmethod 32 | def kill_all(cls): 33 | logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes") 34 | for p in cls._to_kill: 35 | # pylint: disable=W0212 36 | p._clean(verbose=False) 37 | 38 | 39 | class SC2Process: 40 | """ 41 | A class for handling SCII applications. 42 | 43 | :param host: hostname for the url the SCII application will listen to 44 | :param port: the websocket port the SCII application will listen to 45 | :param fullscreen: whether to launch the SCII application in fullscreen or not, defaults to False 46 | :param resolution: (window width, window height) in pixels, defaults to (1024, 768) 47 | :param placement: (x, y) the distances of the SCII app's top left corner from the top left corner of the screen 48 | e.g. (20, 30) is 20 to the right of the screen's left border, and 30 below the top border 49 | :param render: 50 | :param sc2_version: 51 | :param base_build: 52 | :param data_hash: 53 | """ 54 | 55 | def __init__( 56 | self, 57 | host: Optional[str] = None, 58 | port: Optional[int] = None, 59 | fullscreen: bool = False, 60 | resolution: Optional[Union[List[int], Tuple[int, int]]] = None, 61 | placement: Optional[Union[List[int], Tuple[int, int]]] = None, 62 | render: bool = False, 63 | sc2_version: str = None, 64 | base_build: str = None, 65 | data_hash: str = None, 66 | ) -> None: 67 | assert isinstance(host, str) or host is None 68 | assert isinstance(port, int) or port is None 69 | 70 | self._render = render 71 | self._arguments: Dict[str, str] = {"-displayMode": str(int(fullscreen))} 72 | if not fullscreen: 73 | if resolution and len(resolution) == 2: 74 | self._arguments["-windowwidth"] = str(resolution[0]) 75 | self._arguments["-windowheight"] = str(resolution[1]) 76 | if placement and len(placement) == 2: 77 | self._arguments["-windowx"] = str(placement[0]) 78 | self._arguments["-windowy"] = str(placement[1]) 79 | 80 | self._host = host or os.environ.get("SC2CLIENTHOST", "127.0.0.1") 81 | self._serverhost = os.environ.get("SC2SERVERHOST", self._host) 82 | 83 | if port is None: 84 | self._port = portpicker.pick_unused_port() 85 | else: 86 | self._port = port 87 | self._used_portpicker = bool(port is None) 88 | self._tmp_dir = tempfile.mkdtemp(prefix="SC2_") 89 | self._process: subprocess = None 90 | self._session = None 91 | self._ws = None 92 | self._sc2_version = sc2_version 93 | self._base_build = base_build 94 | self._data_hash = data_hash 95 | 96 | async def __aenter__(self) -> Controller: 97 | kill_switch.add(self) 98 | 99 | def signal_handler(*_args): 100 | # unused arguments: signal handling library expects all signal 101 | # callback handlers to accept two positional arguments 102 | kill_switch.kill_all() 103 | 104 | signal.signal(signal.SIGINT, signal_handler) 105 | 106 | try: 107 | self._process = self._launch() 108 | self._ws = await self._connect() 109 | except: 110 | await self._close_connection() 111 | self._clean() 112 | raise 113 | 114 | return Controller(self._ws, self) 115 | 116 | async def __aexit__(self, *args): 117 | await self._close_connection() 118 | kill_switch.kill_all() 119 | signal.signal(signal.SIGINT, signal.SIG_DFL) 120 | 121 | @property 122 | def ws_url(self): 123 | return f"ws://{self._host}:{self._port}/sc2api" 124 | 125 | @property 126 | def versions(self): 127 | """Opens the versions.json file which origins from 128 | https://github.com/Blizzard/s2client-proto/blob/master/buildinfo/versions.json""" 129 | return VERSIONS 130 | 131 | def find_data_hash(self, target_sc2_version: str) -> Optional[str]: 132 | """ Returns the data hash from the matching version string. """ 133 | version: dict 134 | for version in self.versions: 135 | if version["label"] == target_sc2_version: 136 | return version["data-hash"] 137 | return None 138 | 139 | def _launch(self): 140 | if self._base_build: 141 | executable = str(paths.latest_executeble(Paths.BASE / "Versions", self._base_build)) 142 | else: 143 | executable = str(Paths.EXECUTABLE) 144 | if self._port is None: 145 | self._port = portpicker.pick_unused_port() 146 | self._used_portpicker = True 147 | args = paths.get_runner_args(Paths.CWD) + [ 148 | executable, 149 | "-listen", 150 | self._serverhost, 151 | "-port", 152 | str(self._port), 153 | "-dataDir", 154 | str(Paths.BASE), 155 | "-tempDir", 156 | self._tmp_dir, 157 | ] 158 | for arg, value in self._arguments.items(): 159 | args.append(arg) 160 | args.append(value) 161 | if self._sc2_version: 162 | 163 | def special_match(strg: str): 164 | """ Tests if the specified version is in the versions.py dict. """ 165 | for version in self.versions: 166 | if version["label"] == strg: 167 | return True 168 | return False 169 | 170 | valid_version_string = special_match(self._sc2_version) 171 | if valid_version_string: 172 | self._data_hash = self.find_data_hash(self._sc2_version) 173 | assert ( 174 | self._data_hash is not None 175 | ), f"StarCraft 2 Client version ({self._sc2_version}) was not found inside sc2/versions.py file. Please check your spelling or check the versions.py file." 176 | 177 | else: 178 | logger.warning( 179 | f'The submitted version string in sc2.rungame() function call (sc2_version="{self._sc2_version}") was not found in versions.py. Running latest version instead.' 180 | ) 181 | 182 | if self._data_hash: 183 | args.extend(["-dataVersion", self._data_hash]) 184 | 185 | if self._render: 186 | args.extend(["-eglpath", "libEGL.so"]) 187 | 188 | # if logger.getEffectiveLevel() <= logging.DEBUG: 189 | args.append("-verbose") 190 | 191 | sc2_cwd = str(Paths.CWD) if Paths.CWD else None 192 | 193 | if paths.PF in {"WSL1", "WSL2"}: 194 | return wsl.run(args, sc2_cwd) 195 | 196 | return subprocess.Popen( 197 | args, 198 | cwd=sc2_cwd, 199 | # Suppress Wine error messages 200 | stderr=subprocess.DEVNULL 201 | # , env=run_config.env 202 | ) 203 | 204 | async def _connect(self): 205 | # How long it waits for SC2 to start (in seconds) 206 | for i in range(180): 207 | if self._process is None: 208 | # The ._clean() was called, clearing the process 209 | logger.debug("Process cleanup complete, exit") 210 | sys.exit() 211 | 212 | await asyncio.sleep(1) 213 | try: 214 | self._session = aiohttp.ClientSession() 215 | ws = await self._session.ws_connect(self.ws_url, timeout=120) 216 | # FIXME fix deprecation warning in for future aiohttp version 217 | # ws = await self._session.ws_connect( 218 | # self.ws_url, timeout=aiohttp.client_ws.ClientWSTimeout(ws_close=120) 219 | # ) 220 | logger.debug("Websocket connection ready") 221 | return ws 222 | except aiohttp.client_exceptions.ClientConnectorError: 223 | await self._session.close() 224 | if i > 15: 225 | logger.debug("Connection refused (startup not complete (yet))") 226 | 227 | logger.debug("Websocket connection to SC2 process timed out") 228 | raise TimeoutError("Websocket") 229 | 230 | async def _close_connection(self): 231 | logger.info(f"Closing connection at {self._port}...") 232 | 233 | if self._ws is not None: 234 | await self._ws.close() 235 | 236 | if self._session is not None: 237 | await self._session.close() 238 | 239 | # pylint: disable=R0912 240 | def _clean(self, verbose=True): 241 | if verbose: 242 | logger.info("Cleaning up...") 243 | 244 | if self._process is not None: 245 | if paths.PF in {"WSL1", "WSL2"}: 246 | if wsl.kill(self._process): 247 | logger.error("KILLED") 248 | elif self._process.poll() is None: 249 | for _ in range(3): 250 | self._process.terminate() 251 | time.sleep(0.5) 252 | if not self._process or self._process.poll() is not None: 253 | break 254 | else: 255 | self._process.kill() 256 | self._process.wait() 257 | logger.error("KILLED") 258 | # Try to kill wineserver on linux 259 | if paths.PF in {"Linux", "WineLinux"}: 260 | # Command wineserver not detected 261 | with suppress(FileNotFoundError): 262 | with subprocess.Popen(["wineserver", "-k"]) as p: 263 | p.wait() 264 | 265 | if os.path.exists(self._tmp_dir): 266 | shutil.rmtree(self._tmp_dir) 267 | 268 | self._process = None 269 | self._ws = None 270 | if self._used_portpicker and self._port is not None: 271 | portpicker.return_port(self._port) 272 | self._port = None 273 | if verbose: 274 | logger.info("Cleanup complete") 275 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/unit_command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Tuple, Union 4 | 5 | from sc2.constants import COMBINEABLE_ABILITIES 6 | from sc2.ids.ability_id import AbilityId 7 | from sc2.position import Point2 8 | 9 | if TYPE_CHECKING: 10 | from sc2.unit import Unit 11 | 12 | 13 | class UnitCommand: 14 | 15 | def __init__(self, ability: AbilityId, unit: Unit, target: Union[Unit, Point2] = None, queue: bool = False): 16 | """ 17 | :param ability: 18 | :param unit: 19 | :param target: 20 | :param queue: 21 | """ 22 | assert ability in AbilityId, f"ability {ability} is not in AbilityId" 23 | assert unit.__class__.__name__ == "Unit", f"unit {unit} is of type {type(unit)}" 24 | assert any( 25 | [ 26 | target is None, 27 | isinstance(target, Point2), 28 | unit.__class__.__name__ == "Unit", 29 | ] 30 | ), f"target {target} is of type {type(target)}" 31 | assert isinstance(queue, bool), f"queue flag {queue} is of type {type(queue)}" 32 | self.ability = ability 33 | self.unit = unit 34 | self.target = target 35 | self.queue = queue 36 | 37 | @property 38 | def combining_tuple(self) -> Tuple[AbilityId, Union[Unit, Point2], bool, bool]: 39 | return self.ability, self.target, self.queue, self.ability in COMBINEABLE_ABILITIES 40 | 41 | def __repr__(self): 42 | return f"UnitCommand({self.ability}, {self.unit}, {self.target}, {self.queue})" 43 | -------------------------------------------------------------------------------- /bots/basic_bot/sc2/wsl.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0911,W1510 2 | import os 3 | import re 4 | import subprocess 5 | from pathlib import Path, PureWindowsPath 6 | 7 | from loguru import logger 8 | 9 | ## This file is used for compatibility with WSL and shouldn't need to be 10 | ## accessed directly by any bot clients 11 | 12 | 13 | def win_path_to_wsl_path(path): 14 | """Convert a path like C:\\foo to /mnt/c/foo""" 15 | return Path("/mnt") / PureWindowsPath(re.sub("^([A-Z]):", lambda m: m.group(1).lower(), path)) 16 | 17 | 18 | def wsl_path_to_win_path(path): 19 | """Convert a path like /mnt/c/foo to C:\\foo""" 20 | return PureWindowsPath(re.sub("^/mnt/([a-z])", lambda m: m.group(1).upper() + ":", path)) 21 | 22 | 23 | def get_wsl_home(): 24 | """Get home directory of from Windows, even if run in WSL""" 25 | proc = subprocess.run(["powershell.exe", "-Command", "Write-Host -NoNewLine $HOME"], capture_output=True) 26 | 27 | if proc.returncode != 0: 28 | return None 29 | 30 | return win_path_to_wsl_path(proc.stdout.decode("utf-8")) 31 | 32 | 33 | RUN_SCRIPT = """$proc = Start-Process -NoNewWindow -PassThru "%s" "%s" 34 | if ($proc) { 35 | Write-Host $proc.id 36 | exit $proc.ExitCode 37 | } else { 38 | exit 1 39 | }""" 40 | 41 | 42 | def run(popen_args, sc2_cwd): 43 | """Run SC2 in Windows and get the pid so that it can be killed later.""" 44 | path = wsl_path_to_win_path(popen_args[0]) 45 | args = " ".join(popen_args[1:]) 46 | 47 | return subprocess.Popen( 48 | ["powershell.exe", "-Command", RUN_SCRIPT % (path, args)], 49 | cwd=sc2_cwd, 50 | stdout=subprocess.PIPE, 51 | universal_newlines=True, 52 | bufsize=1, 53 | ) 54 | 55 | 56 | def kill(wsl_process): 57 | """Needed to kill a process started with WSL. Returns true if killed successfully.""" 58 | # HACK: subprocess and WSL1 appear to have a nasty interaction where 59 | # any streams are never closed and the process is never considered killed, 60 | # despite having an exit code (this works on WSL2 as well, but isn't 61 | # necessary). As a result, 62 | # 1: We need to read using readline (to make sure we block long enough to 63 | # get the exit code in the rare case where the user immediately hits ^C) 64 | out = wsl_process.stdout.readline().rstrip() 65 | # 2: We need to use __exit__, since kill() calls send_signal(), which thinks 66 | # the process has already exited! 67 | wsl_process.__exit__(None, None, None) 68 | proc = subprocess.run(["taskkill.exe", "-f", "-pid", out], capture_output=True) 69 | return proc.returncode == 0 # Returns 128 on failure 70 | 71 | 72 | def detect(): 73 | """Detect the current running version of WSL, and bail out if it doesn't exist""" 74 | # Allow disabling WSL detection with an environment variable 75 | if os.getenv("SC2_WSL_DETECT", "1") == "0": 76 | return None 77 | 78 | wsl_name = os.environ.get("WSL_DISTRO_NAME") 79 | if not wsl_name: 80 | return None 81 | 82 | try: 83 | wsl_proc = subprocess.run(["wsl.exe", "--list", "--running", "--verbose"], capture_output=True) 84 | except (OSError, ValueError): 85 | return None 86 | if wsl_proc.returncode != 0: 87 | return None 88 | 89 | # WSL.exe returns a bunch of null characters for some reason, as well as 90 | # windows-style linebreaks. It's inconsistent about how many \rs it uses 91 | # and this could change in the future, so strip out all junk and split by 92 | # Unix-style newlines for safety's sake. 93 | lines = re.sub(r"\000|\r", "", wsl_proc.stdout.decode("utf-8")).split("\n") 94 | 95 | def line_has_proc(ln): 96 | return re.search("^\\s*[*]?\\s+" + wsl_name, ln) 97 | 98 | def line_version(ln): 99 | return re.sub("^.*\\s+(\\d+)\\s*$", "\\1", ln) 100 | 101 | versions = [line_version(ln) for ln in lines if line_has_proc(ln)] 102 | 103 | try: 104 | version = versions[0] 105 | if int(version) not in [1, 2]: 106 | return None 107 | except (ValueError, IndexError): 108 | return None 109 | 110 | logger.info(f"WSL version {version} detected") 111 | 112 | if version == "2" and not (os.environ.get("SC2CLIENTHOST") and os.environ.get("SC2SERVERHOST")): 113 | logger.warning("You appear to be running WSL2 without your hosts configured correctly.") 114 | logger.warning("This may result in SC2 staying on a black screen and not connecting to your bot.") 115 | logger.warning("Please see the python-sc2 README for WSL2 configuration instructions.") 116 | 117 | return "WSL" + version 118 | -------------------------------------------------------------------------------- /bots/loser_bot/README.md: -------------------------------------------------------------------------------- 1 | ## Test Bot 2 | ### Normal Bot Simulation 3 | 4 | This is a normal bot that does nothing but mine minerals. 5 | This bot should lose against any other bot that attacks -------------------------------------------------------------------------------- /bots/loser_bot/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0212 2 | import argparse 3 | import asyncio 4 | 5 | import aiohttp 6 | from loguru import logger 7 | 8 | import sc2 9 | from sc2.client import Client 10 | from sc2.protocol import ConnectionAlreadyClosed 11 | 12 | 13 | # Run ladder game 14 | # This lets python-sc2 connect to a LadderManager game: https://github.com/Cryptyc/Sc2LadderServer 15 | # Based on: https://github.com/Dentosal/python-sc2/blob/master/examples/run_external.py 16 | def run_ladder_game(bot): 17 | # Load command line arguments 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument("--GamePort", type=int, nargs="?", help="Game port") 20 | parser.add_argument("--StartPort", type=int, nargs="?", help="Start port") 21 | parser.add_argument("--LadderServer", type=str, nargs="?", help="Ladder server") 22 | parser.add_argument("--ComputerOpponent", type=str, nargs="?", help="Computer opponent") 23 | parser.add_argument("--ComputerRace", type=str, nargs="?", help="Computer race") 24 | parser.add_argument("--ComputerDifficulty", type=str, nargs="?", help="Computer difficulty") 25 | parser.add_argument("--OpponentId", type=str, nargs="?", help="Opponent ID") 26 | parser.add_argument("--RealTime", action="store_true", help="Real time flag") 27 | args, _unknown = parser.parse_known_args() 28 | 29 | if args.LadderServer is None: 30 | host = "127.0.0.1" 31 | else: 32 | host = args.LadderServer 33 | 34 | host_port = args.GamePort 35 | lan_port = args.StartPort 36 | 37 | # Add opponent_id to the bot class (accessed through self.opponent_id) 38 | bot.ai.opponent_id = args.OpponentId 39 | 40 | realtime = args.RealTime 41 | 42 | # Port config 43 | if lan_port is None: 44 | portconfig = None 45 | else: 46 | ports = [lan_port + p for p in range(1, 6)] 47 | 48 | portconfig = sc2.portconfig.Portconfig() 49 | portconfig.server = [ports[1], ports[2]] 50 | portconfig.players = [[ports[3], ports[4]]] 51 | 52 | # Join ladder game 53 | g = join_ladder_game(host=host, port=host_port, players=[bot], realtime=realtime, portconfig=portconfig) 54 | 55 | # Run it 56 | result = asyncio.get_event_loop().run_until_complete(g) 57 | return result, args.OpponentId 58 | 59 | 60 | # Modified version of sc2.main._join_game to allow custom host and port, and to not spawn an additional sc2process (thanks to alkurbatov for fix) 61 | async def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, game_time_limit=None): 62 | ws_url = f"ws://{host}:{port}/sc2api" 63 | ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120) 64 | client = Client(ws_connection) 65 | try: 66 | result = await sc2.main._play_game(players[0], client, realtime, portconfig, game_time_limit) 67 | if save_replay_as is not None: 68 | await client.save_replay(save_replay_as) 69 | # await client.leave() 70 | # await client.quit() 71 | except ConnectionAlreadyClosed: 72 | logger.error("Connection was closed before the game ended") 73 | return None 74 | finally: 75 | ws_connection.close() 76 | 77 | return result -------------------------------------------------------------------------------- /bots/loser_bot/bot.py: -------------------------------------------------------------------------------- 1 | from sc2.bot_ai import BotAI 2 | 3 | 4 | class TestBot(BotAI): 5 | async def on_step(self, iteration): 6 | if iteration == 0: 7 | self.game_step = 100 8 | -------------------------------------------------------------------------------- /bots/loser_bot/run.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E0401 2 | import sys 3 | 4 | from __init__ import run_ladder_game 5 | 6 | # Load bot 7 | from bot import TestBot 8 | 9 | from sc2 import maps 10 | from sc2.data import Difficulty, Race 11 | from sc2.main import run_game 12 | from sc2.player import Bot, Computer 13 | 14 | bot = Bot(Race.Terran, TestBot()) 15 | 16 | # Start game 17 | if __name__ == "__main__": 18 | if "--LadderServer" in sys.argv: 19 | # Ladder game started by LadderManager 20 | print("Starting ladder game...") 21 | result, opponentid = run_ladder_game(bot) 22 | print(result, " against opponent ", opponentid) 23 | else: 24 | # Local game 25 | print("Starting local game...") 26 | run_game(maps.get("Abyssal Reef LE"), [bot, Computer(Race.Protoss, Difficulty.VeryHard)], realtime=True) -------------------------------------------------------------------------------- /bots/loser_bot/sc2/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def is_submodule(path): 5 | if path.is_file(): 6 | return path.suffix == ".py" and path.stem != "__init__" 7 | if path.is_dir(): 8 | return (path / "__init__.py").exists() 9 | return False 10 | 11 | 12 | __all__ = [p.stem for p in Path(__file__).parent.iterdir() if is_submodule(p)] 13 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/action.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import groupby 4 | from typing import TYPE_CHECKING, Union 5 | 6 | from s2clientprotocol import raw_pb2 as raw_pb 7 | 8 | from sc2.position import Point2 9 | from sc2.unit import Unit 10 | 11 | if TYPE_CHECKING: 12 | from sc2.ids.ability_id import AbilityId 13 | from sc2.unit_command import UnitCommand 14 | 15 | 16 | # pylint: disable=R0912 17 | def combine_actions(action_iter): 18 | """ 19 | Example input: 20 | [ 21 | # Each entry in the list is a unit command, with an ability, unit, target, and queue=boolean 22 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hive', tag=4353687554), None, False), 23 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Lair', tag=4359979012), None, False), 24 | UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hatchery', tag=4359454723), None, False), 25 | ] 26 | """ 27 | for key, items in groupby(action_iter, key=lambda a: a.combining_tuple): 28 | ability: AbilityId 29 | target: Union[None, Point2, Unit] 30 | queue: bool 31 | # See constants.py for combineable abilities 32 | combineable: bool 33 | ability, target, queue, combineable = key 34 | 35 | if combineable: 36 | # Combine actions with no target, e.g. lift, burrowup, burrowdown, siege, unsiege, uproot spines 37 | cmd = raw_pb.ActionRawUnitCommand( 38 | ability_id=ability.value, unit_tags={u.unit.tag 39 | for u in items}, queue_command=queue 40 | ) 41 | # Combine actions with target point, e.g. attack_move or move commands on a position 42 | if isinstance(target, Point2): 43 | cmd.target_world_space_pos.x = target.x 44 | cmd.target_world_space_pos.y = target.y 45 | # Combine actions with target unit, e.g. attack commands directly on a unit 46 | elif isinstance(target, Unit): 47 | cmd.target_unit_tag = target.tag 48 | elif target is not None: 49 | raise RuntimeError(f"Must target a unit, point or None, found '{target !r}'") 50 | 51 | yield raw_pb.ActionRaw(unit_command=cmd) 52 | 53 | else: 54 | """ 55 | Return one action for each unit; this is required for certain commands that would otherwise be grouped, and only executed once 56 | Examples: 57 | Select 3 hatcheries, build a queen with each hatch - the grouping function would group these unit tags and only issue one train command once to all 3 unit tags - resulting in one total train command 58 | I imagine the same thing would happen to certain other abilities: Battlecruiser yamato on same target, queen transfuse on same target, ghost snipe on same target, all build commands with the same unit type and also all morphs (zergling to banelings) 59 | However, other abilities can and should be grouped, see constants.py 'COMBINEABLE_ABILITIES' 60 | """ 61 | u: UnitCommand 62 | if target is None: 63 | for u in items: 64 | cmd = raw_pb.ActionRawUnitCommand( 65 | ability_id=ability.value, unit_tags={u.unit.tag}, queue_command=queue 66 | ) 67 | yield raw_pb.ActionRaw(unit_command=cmd) 68 | elif isinstance(target, Point2): 69 | for u in items: 70 | cmd = raw_pb.ActionRawUnitCommand( 71 | ability_id=ability.value, 72 | unit_tags={u.unit.tag}, 73 | queue_command=queue, 74 | target_world_space_pos=target.as_Point2D, 75 | ) 76 | yield raw_pb.ActionRaw(unit_command=cmd) 77 | 78 | elif isinstance(target, Unit): 79 | for u in items: 80 | cmd = raw_pb.ActionRawUnitCommand( 81 | ability_id=ability.value, 82 | unit_tags={u.unit.tag}, 83 | queue_command=queue, 84 | target_unit_tag=target.tag, 85 | ) 86 | yield raw_pb.ActionRaw(unit_command=cmd) 87 | else: 88 | raise RuntimeError(f"Must target a unit, point or None, found '{target !r}'") 89 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Callable, TypeVar 4 | 5 | if TYPE_CHECKING: 6 | from sc2.bot_ai import BotAI 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | class property_cache_once_per_frame(property): 12 | """This decorator caches the return value for one game loop, 13 | then clears it if it is accessed in a different game loop. 14 | Only works on properties of the bot object, because it requires 15 | access to self.state.game_loop 16 | 17 | This decorator compared to the above runs a little faster, however you should only use this decorator if you are sure that you do not modify the mutable once it is calculated and cached. 18 | 19 | Copied and modified from https://tedboy.github.io/flask/_modules/werkzeug/utils.html#cached_property 20 | # """ 21 | 22 | def __init__(self, func: Callable[[BotAI], T], name=None): 23 | # pylint: disable=W0231 24 | self.__name__ = name or func.__name__ 25 | self.__frame__ = f"__frame__{self.__name__}" 26 | self.func = func 27 | 28 | def __set__(self, obj: BotAI, value: T): 29 | obj.cache[self.__name__] = value 30 | obj.cache[self.__frame__] = obj.state.game_loop 31 | 32 | def __get__(self, obj: BotAI, _type=None) -> T: 33 | value = obj.cache.get(self.__name__, None) 34 | bot_frame = obj.state.game_loop 35 | if value is None or obj.cache[self.__frame__] < bot_frame: 36 | value = self.func(obj) 37 | obj.cache[self.__name__] = value 38 | obj.cache[self.__frame__] = bot_frame 39 | return value 40 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/controller.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | from s2clientprotocol import sc2api_pb2 as sc_pb 6 | 7 | from sc2.player import Computer 8 | from sc2.protocol import Protocol 9 | 10 | 11 | class Controller(Protocol): 12 | 13 | def __init__(self, ws, process): 14 | super().__init__(ws) 15 | self._process = process 16 | 17 | @property 18 | def running(self): 19 | # pylint: disable=W0212 20 | return self._process._process is not None 21 | 22 | async def create_game(self, game_map, players, realtime: bool, random_seed=None, disable_fog=None): 23 | req = sc_pb.RequestCreateGame( 24 | local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)), realtime=realtime, disable_fog=disable_fog 25 | ) 26 | if random_seed is not None: 27 | req.random_seed = random_seed 28 | 29 | for player in players: 30 | p = req.player_setup.add() 31 | p.type = player.type.value 32 | if isinstance(player, Computer): 33 | p.race = player.race.value 34 | p.difficulty = player.difficulty.value 35 | p.ai_build = player.ai_build.value 36 | 37 | logger.info("Creating new game") 38 | logger.info(f"Map: {game_map.name}") 39 | logger.info(f"Players: {', '.join(str(p) for p in players)}") 40 | result = await self._execute(create_game=req) 41 | return result 42 | 43 | async def request_available_maps(self): 44 | req = sc_pb.RequestAvailableMaps() 45 | result = await self._execute(available_maps=req) 46 | return result 47 | 48 | async def request_save_map(self, download_path: str): 49 | """ Not working on linux. """ 50 | req = sc_pb.RequestSaveMap(map_path=download_path) 51 | result = await self._execute(save_map=req) 52 | return result 53 | 54 | async def request_replay_info(self, replay_path: str): 55 | """ Not working on linux. """ 56 | req = sc_pb.RequestReplayInfo(replay_path=replay_path, download_data=False) 57 | result = await self._execute(replay_info=req) 58 | return result 59 | 60 | async def start_replay(self, replay_path: str, realtime: bool, observed_id: int = 0): 61 | ifopts = sc_pb.InterfaceOptions( 62 | raw=True, score=True, show_cloaked=True, raw_affects_selection=True, raw_crop_to_playable_area=False 63 | ) 64 | if platform.system() == "Linux": 65 | replay_name = Path(replay_path).name 66 | home_replay_folder = Path.home() / "Documents" / "StarCraft II" / "Replays" 67 | if str(home_replay_folder / replay_name) != replay_path: 68 | logger.warning( 69 | f"Linux detected, please put your replay in your home directory at {home_replay_folder}. It was detected at {replay_path}" 70 | ) 71 | raise FileNotFoundError 72 | replay_path = replay_name 73 | 74 | req = sc_pb.RequestStartReplay( 75 | replay_path=replay_path, observed_player_id=observed_id, realtime=realtime, options=ifopts 76 | ) 77 | 78 | result = await self._execute(start_replay=req) 79 | assert result.status == 4, f"{result.start_replay.error} - {result.start_replay.error_details}" 80 | return result 81 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/data.py: -------------------------------------------------------------------------------- 1 | """ For the list of enums, see here 2 | 3 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_gametypes.h 4 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_action.h 5 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_unit.h 6 | https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_data.h 7 | """ 8 | import enum 9 | from typing import Dict, Set 10 | 11 | from s2clientprotocol import common_pb2 as common_pb 12 | from s2clientprotocol import data_pb2 as data_pb 13 | from s2clientprotocol import error_pb2 as error_pb 14 | from s2clientprotocol import raw_pb2 as raw_pb 15 | from s2clientprotocol import sc2api_pb2 as sc_pb 16 | 17 | from sc2.ids.ability_id import AbilityId 18 | from sc2.ids.unit_typeid import UnitTypeId 19 | 20 | CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items()) 21 | 22 | PlayerType = enum.Enum("PlayerType", sc_pb.PlayerType.items()) 23 | Difficulty = enum.Enum("Difficulty", sc_pb.Difficulty.items()) 24 | AIBuild = enum.Enum("AIBuild", sc_pb.AIBuild.items()) 25 | Status = enum.Enum("Status", sc_pb.Status.items()) 26 | Result = enum.Enum("Result", sc_pb.Result.items()) 27 | Alert = enum.Enum("Alert", sc_pb.Alert.items()) 28 | ChatChannel = enum.Enum("ChatChannel", sc_pb.ActionChat.Channel.items()) 29 | 30 | Race = enum.Enum("Race", common_pb.Race.items()) 31 | 32 | DisplayType = enum.Enum("DisplayType", raw_pb.DisplayType.items()) 33 | Alliance = enum.Enum("Alliance", raw_pb.Alliance.items()) 34 | CloakState = enum.Enum("CloakState", raw_pb.CloakState.items()) 35 | 36 | Attribute = enum.Enum("Attribute", data_pb.Attribute.items()) 37 | TargetType = enum.Enum("TargetType", data_pb.Weapon.TargetType.items()) 38 | Target = enum.Enum("Target", data_pb.AbilityData.Target.items()) 39 | 40 | ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items()) 41 | 42 | race_worker: Dict[Race, UnitTypeId] = { 43 | Race.Protoss: UnitTypeId.PROBE, 44 | Race.Terran: UnitTypeId.SCV, 45 | Race.Zerg: UnitTypeId.DRONE, 46 | } 47 | 48 | race_townhalls: Dict[Race, Set[UnitTypeId]] = { 49 | Race.Protoss: {UnitTypeId.NEXUS}, 50 | Race.Terran: { 51 | UnitTypeId.COMMANDCENTER, 52 | UnitTypeId.ORBITALCOMMAND, 53 | UnitTypeId.PLANETARYFORTRESS, 54 | UnitTypeId.COMMANDCENTERFLYING, 55 | UnitTypeId.ORBITALCOMMANDFLYING, 56 | }, 57 | Race.Zerg: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE}, 58 | Race.Random: { 59 | # Protoss 60 | UnitTypeId.NEXUS, 61 | # Terran 62 | UnitTypeId.COMMANDCENTER, 63 | UnitTypeId.ORBITALCOMMAND, 64 | UnitTypeId.PLANETARYFORTRESS, 65 | UnitTypeId.COMMANDCENTERFLYING, 66 | UnitTypeId.ORBITALCOMMANDFLYING, 67 | # Zerg 68 | UnitTypeId.HATCHERY, 69 | UnitTypeId.LAIR, 70 | UnitTypeId.HIVE, 71 | }, 72 | } 73 | 74 | warpgate_abilities: Dict[AbilityId, AbilityId] = { 75 | AbilityId.GATEWAYTRAIN_ZEALOT: AbilityId.WARPGATETRAIN_ZEALOT, 76 | AbilityId.GATEWAYTRAIN_STALKER: AbilityId.WARPGATETRAIN_STALKER, 77 | AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: AbilityId.WARPGATETRAIN_HIGHTEMPLAR, 78 | AbilityId.GATEWAYTRAIN_DARKTEMPLAR: AbilityId.WARPGATETRAIN_DARKTEMPLAR, 79 | AbilityId.GATEWAYTRAIN_SENTRY: AbilityId.WARPGATETRAIN_SENTRY, 80 | AbilityId.TRAIN_ADEPT: AbilityId.TRAINWARP_ADEPT, 81 | } 82 | 83 | race_gas: Dict[Race, UnitTypeId] = { 84 | Race.Protoss: UnitTypeId.ASSIMILATOR, 85 | Race.Terran: UnitTypeId.REFINERY, 86 | Race.Zerg: UnitTypeId.EXTRACTOR, 87 | } 88 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/dicts/__init__.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_dicts_from_data_json.py" 3 | 4 | __all__ = [ 5 | 'generic_redirect_abilities', 'unit_abilities', 'unit_research_abilities', 'unit_tech_alias', 6 | 'unit_train_build_abilities', 'unit_trained_from', 'unit_unit_alias', 'upgrade_researched_from' 7 | ] 8 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/dicts/unit_tech_alias.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict, Set 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_TECH_ALIAS: Dict[UnitTypeId, Set[UnitTypeId]] = { 12 | UnitTypeId.BARRACKSFLYING: {UnitTypeId.BARRACKS}, 13 | UnitTypeId.BARRACKSREACTOR: {UnitTypeId.REACTOR}, 14 | UnitTypeId.BARRACKSTECHLAB: {UnitTypeId.TECHLAB}, 15 | UnitTypeId.COMMANDCENTERFLYING: {UnitTypeId.COMMANDCENTER}, 16 | UnitTypeId.CREEPTUMORBURROWED: {UnitTypeId.CREEPTUMOR}, 17 | UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.CREEPTUMOR}, 18 | UnitTypeId.FACTORYFLYING: {UnitTypeId.FACTORY}, 19 | UnitTypeId.FACTORYREACTOR: {UnitTypeId.REACTOR}, 20 | UnitTypeId.FACTORYTECHLAB: {UnitTypeId.TECHLAB}, 21 | UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, 22 | UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR}, 23 | UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, 24 | UnitTypeId.LIBERATORAG: {UnitTypeId.LIBERATOR}, 25 | UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, 26 | UnitTypeId.ORBITALCOMMANDFLYING: {UnitTypeId.COMMANDCENTER}, 27 | UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, 28 | UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD}, 29 | UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.OVERLORD}, 30 | UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, 31 | UnitTypeId.PYLONOVERCHARGED: {UnitTypeId.PYLON}, 32 | UnitTypeId.QUEENBURROWED: {UnitTypeId.QUEEN}, 33 | UnitTypeId.SIEGETANKSIEGED: {UnitTypeId.SIEGETANK}, 34 | UnitTypeId.STARPORTFLYING: {UnitTypeId.STARPORT}, 35 | UnitTypeId.STARPORTREACTOR: {UnitTypeId.REACTOR}, 36 | UnitTypeId.STARPORTTECHLAB: {UnitTypeId.TECHLAB}, 37 | UnitTypeId.SUPPLYDEPOTLOWERED: {UnitTypeId.SUPPLYDEPOT}, 38 | UnitTypeId.THORAP: {UnitTypeId.THOR}, 39 | UnitTypeId.VIKINGASSAULT: {UnitTypeId.VIKING}, 40 | UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKING}, 41 | UnitTypeId.WARPGATE: {UnitTypeId.GATEWAY}, 42 | UnitTypeId.WARPPRISMPHASING: {UnitTypeId.WARPPRISM}, 43 | UnitTypeId.WIDOWMINEBURROWED: {UnitTypeId.WIDOWMINE} 44 | } 45 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/dicts/unit_trained_from.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict, Set 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_TRAINED_FROM: Dict[UnitTypeId, Set[UnitTypeId]] = { 12 | UnitTypeId.ADEPT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 13 | UnitTypeId.ARMORY: {UnitTypeId.SCV}, 14 | UnitTypeId.ASSIMILATOR: {UnitTypeId.PROBE}, 15 | UnitTypeId.AUTOTURRET: {UnitTypeId.RAVEN}, 16 | UnitTypeId.BANELING: {UnitTypeId.ZERGLING}, 17 | UnitTypeId.BANELINGNEST: {UnitTypeId.DRONE}, 18 | UnitTypeId.BANSHEE: {UnitTypeId.STARPORT}, 19 | UnitTypeId.BARRACKS: {UnitTypeId.SCV}, 20 | UnitTypeId.BATTLECRUISER: {UnitTypeId.STARPORT}, 21 | UnitTypeId.BROODLORD: {UnitTypeId.CORRUPTOR}, 22 | UnitTypeId.BUNKER: {UnitTypeId.SCV}, 23 | UnitTypeId.CARRIER: {UnitTypeId.STARGATE}, 24 | UnitTypeId.CHANGELING: {UnitTypeId.OVERSEER, UnitTypeId.OVERSEERSIEGEMODE}, 25 | UnitTypeId.COLOSSUS: {UnitTypeId.ROBOTICSFACILITY}, 26 | UnitTypeId.COMMANDCENTER: {UnitTypeId.SCV}, 27 | UnitTypeId.CORRUPTOR: {UnitTypeId.LARVA}, 28 | UnitTypeId.CREEPTUMOR: { 29 | UnitTypeId.CREEPTUMOR, UnitTypeId.CREEPTUMORBURROWED, UnitTypeId.CREEPTUMORQUEEN, UnitTypeId.QUEEN 30 | }, 31 | UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.QUEEN}, 32 | UnitTypeId.CYBERNETICSCORE: {UnitTypeId.PROBE}, 33 | UnitTypeId.CYCLONE: {UnitTypeId.FACTORY}, 34 | UnitTypeId.DARKSHRINE: {UnitTypeId.PROBE}, 35 | UnitTypeId.DARKTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 36 | UnitTypeId.DISRUPTOR: {UnitTypeId.ROBOTICSFACILITY}, 37 | UnitTypeId.DRONE: {UnitTypeId.LARVA}, 38 | UnitTypeId.ENGINEERINGBAY: {UnitTypeId.SCV}, 39 | UnitTypeId.EVOLUTIONCHAMBER: {UnitTypeId.DRONE}, 40 | UnitTypeId.EXTRACTOR: {UnitTypeId.DRONE}, 41 | UnitTypeId.FACTORY: {UnitTypeId.SCV}, 42 | UnitTypeId.FLEETBEACON: {UnitTypeId.PROBE}, 43 | UnitTypeId.FORGE: {UnitTypeId.PROBE}, 44 | UnitTypeId.FUSIONCORE: {UnitTypeId.SCV}, 45 | UnitTypeId.GATEWAY: {UnitTypeId.PROBE}, 46 | UnitTypeId.GHOST: {UnitTypeId.BARRACKS}, 47 | UnitTypeId.GHOSTACADEMY: {UnitTypeId.SCV}, 48 | UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE}, 49 | UnitTypeId.HATCHERY: {UnitTypeId.DRONE}, 50 | UnitTypeId.HELLION: {UnitTypeId.FACTORY}, 51 | UnitTypeId.HELLIONTANK: {UnitTypeId.FACTORY}, 52 | UnitTypeId.HIGHTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 53 | UnitTypeId.HIVE: {UnitTypeId.LAIR}, 54 | UnitTypeId.HYDRALISK: {UnitTypeId.LARVA}, 55 | UnitTypeId.HYDRALISKDEN: {UnitTypeId.DRONE}, 56 | UnitTypeId.IMMORTAL: {UnitTypeId.ROBOTICSFACILITY}, 57 | UnitTypeId.INFESTATIONPIT: {UnitTypeId.DRONE}, 58 | UnitTypeId.INFESTOR: {UnitTypeId.LARVA}, 59 | UnitTypeId.LAIR: {UnitTypeId.HATCHERY}, 60 | UnitTypeId.LIBERATOR: {UnitTypeId.STARPORT}, 61 | UnitTypeId.LOCUSTMPFLYING: {UnitTypeId.SWARMHOSTBURROWEDMP, UnitTypeId.SWARMHOSTMP}, 62 | UnitTypeId.LURKERDENMP: {UnitTypeId.DRONE}, 63 | UnitTypeId.LURKERMP: {UnitTypeId.HYDRALISK}, 64 | UnitTypeId.MARAUDER: {UnitTypeId.BARRACKS}, 65 | UnitTypeId.MARINE: {UnitTypeId.BARRACKS}, 66 | UnitTypeId.MEDIVAC: {UnitTypeId.STARPORT}, 67 | UnitTypeId.MISSILETURRET: {UnitTypeId.SCV}, 68 | UnitTypeId.MOTHERSHIP: {UnitTypeId.NEXUS}, 69 | UnitTypeId.MUTALISK: {UnitTypeId.LARVA}, 70 | UnitTypeId.NEXUS: {UnitTypeId.PROBE}, 71 | UnitTypeId.NYDUSCANAL: {UnitTypeId.NYDUSNETWORK}, 72 | UnitTypeId.NYDUSNETWORK: {UnitTypeId.DRONE}, 73 | UnitTypeId.OBSERVER: {UnitTypeId.ROBOTICSFACILITY}, 74 | UnitTypeId.ORACLE: {UnitTypeId.STARGATE}, 75 | UnitTypeId.ORACLESTASISTRAP: {UnitTypeId.ORACLE}, 76 | UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER}, 77 | UnitTypeId.OVERLORD: {UnitTypeId.LARVA}, 78 | UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD}, 79 | UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD, UnitTypeId.OVERLORDTRANSPORT}, 80 | UnitTypeId.PHOENIX: {UnitTypeId.STARGATE}, 81 | UnitTypeId.PHOTONCANNON: {UnitTypeId.PROBE}, 82 | UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER}, 83 | UnitTypeId.PROBE: {UnitTypeId.NEXUS}, 84 | UnitTypeId.PYLON: {UnitTypeId.PROBE}, 85 | UnitTypeId.QUEEN: {UnitTypeId.HATCHERY, UnitTypeId.HIVE, UnitTypeId.LAIR}, 86 | UnitTypeId.RAVAGER: {UnitTypeId.ROACH}, 87 | UnitTypeId.RAVEN: {UnitTypeId.STARPORT}, 88 | UnitTypeId.REAPER: {UnitTypeId.BARRACKS}, 89 | UnitTypeId.REFINERY: {UnitTypeId.SCV}, 90 | UnitTypeId.ROACH: {UnitTypeId.LARVA}, 91 | UnitTypeId.ROACHWARREN: {UnitTypeId.DRONE}, 92 | UnitTypeId.ROBOTICSBAY: {UnitTypeId.PROBE}, 93 | UnitTypeId.ROBOTICSFACILITY: {UnitTypeId.PROBE}, 94 | UnitTypeId.SCV: {UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS}, 95 | UnitTypeId.SENSORTOWER: {UnitTypeId.SCV}, 96 | UnitTypeId.SENTRY: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 97 | UnitTypeId.SHIELDBATTERY: {UnitTypeId.PROBE}, 98 | UnitTypeId.SIEGETANK: {UnitTypeId.FACTORY}, 99 | UnitTypeId.SPAWNINGPOOL: {UnitTypeId.DRONE}, 100 | UnitTypeId.SPINECRAWLER: {UnitTypeId.DRONE}, 101 | UnitTypeId.SPIRE: {UnitTypeId.DRONE}, 102 | UnitTypeId.SPORECRAWLER: {UnitTypeId.DRONE}, 103 | UnitTypeId.STALKER: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 104 | UnitTypeId.STARGATE: {UnitTypeId.PROBE}, 105 | UnitTypeId.STARPORT: {UnitTypeId.SCV}, 106 | UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SCV}, 107 | UnitTypeId.SWARMHOSTMP: {UnitTypeId.LARVA}, 108 | UnitTypeId.TEMPEST: {UnitTypeId.STARGATE}, 109 | UnitTypeId.TEMPLARARCHIVE: {UnitTypeId.PROBE}, 110 | UnitTypeId.THOR: {UnitTypeId.FACTORY}, 111 | UnitTypeId.TWILIGHTCOUNCIL: {UnitTypeId.PROBE}, 112 | UnitTypeId.ULTRALISK: {UnitTypeId.LARVA}, 113 | UnitTypeId.ULTRALISKCAVERN: {UnitTypeId.DRONE}, 114 | UnitTypeId.VIKINGFIGHTER: {UnitTypeId.STARPORT}, 115 | UnitTypeId.VIPER: {UnitTypeId.LARVA}, 116 | UnitTypeId.VOIDRAY: {UnitTypeId.STARGATE}, 117 | UnitTypeId.WARPPRISM: {UnitTypeId.ROBOTICSFACILITY}, 118 | UnitTypeId.WIDOWMINE: {UnitTypeId.FACTORY}, 119 | UnitTypeId.ZEALOT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE}, 120 | UnitTypeId.ZERGLING: {UnitTypeId.LARVA} 121 | } 122 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/dicts/unit_unit_alias.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | 8 | # from ..ids.buff_id import BuffId 9 | # from ..ids.effect_id import EffectId 10 | 11 | UNIT_UNIT_ALIAS: Dict[UnitTypeId, UnitTypeId] = { 12 | UnitTypeId.ADEPTPHASESHIFT: UnitTypeId.ADEPT, 13 | UnitTypeId.BANELINGBURROWED: UnitTypeId.BANELING, 14 | UnitTypeId.BARRACKSFLYING: UnitTypeId.BARRACKS, 15 | UnitTypeId.CHANGELINGMARINE: UnitTypeId.CHANGELING, 16 | UnitTypeId.CHANGELINGMARINESHIELD: UnitTypeId.CHANGELING, 17 | UnitTypeId.CHANGELINGZEALOT: UnitTypeId.CHANGELING, 18 | UnitTypeId.CHANGELINGZERGLING: UnitTypeId.CHANGELING, 19 | UnitTypeId.CHANGELINGZERGLINGWINGS: UnitTypeId.CHANGELING, 20 | UnitTypeId.COMMANDCENTERFLYING: UnitTypeId.COMMANDCENTER, 21 | UnitTypeId.CREEPTUMORBURROWED: UnitTypeId.CREEPTUMOR, 22 | UnitTypeId.CREEPTUMORQUEEN: UnitTypeId.CREEPTUMOR, 23 | UnitTypeId.DRONEBURROWED: UnitTypeId.DRONE, 24 | UnitTypeId.FACTORYFLYING: UnitTypeId.FACTORY, 25 | UnitTypeId.GHOSTNOVA: UnitTypeId.GHOST, 26 | UnitTypeId.HERCPLACEMENT: UnitTypeId.HERC, 27 | UnitTypeId.HYDRALISKBURROWED: UnitTypeId.HYDRALISK, 28 | UnitTypeId.INFESTORBURROWED: UnitTypeId.INFESTOR, 29 | UnitTypeId.INFESTORTERRANBURROWED: UnitTypeId.INFESTORTERRAN, 30 | UnitTypeId.LIBERATORAG: UnitTypeId.LIBERATOR, 31 | UnitTypeId.LOCUSTMPFLYING: UnitTypeId.LOCUSTMP, 32 | UnitTypeId.LURKERMPBURROWED: UnitTypeId.LURKERMP, 33 | UnitTypeId.OBSERVERSIEGEMODE: UnitTypeId.OBSERVER, 34 | UnitTypeId.ORBITALCOMMANDFLYING: UnitTypeId.ORBITALCOMMAND, 35 | UnitTypeId.OVERSEERSIEGEMODE: UnitTypeId.OVERSEER, 36 | UnitTypeId.PYLONOVERCHARGED: UnitTypeId.PYLON, 37 | UnitTypeId.QUEENBURROWED: UnitTypeId.QUEEN, 38 | UnitTypeId.RAVAGERBURROWED: UnitTypeId.RAVAGER, 39 | UnitTypeId.ROACHBURROWED: UnitTypeId.ROACH, 40 | UnitTypeId.SIEGETANKSIEGED: UnitTypeId.SIEGETANK, 41 | UnitTypeId.SPINECRAWLERUPROOTED: UnitTypeId.SPINECRAWLER, 42 | UnitTypeId.SPORECRAWLERUPROOTED: UnitTypeId.SPORECRAWLER, 43 | UnitTypeId.STARPORTFLYING: UnitTypeId.STARPORT, 44 | UnitTypeId.SUPPLYDEPOTLOWERED: UnitTypeId.SUPPLYDEPOT, 45 | UnitTypeId.SWARMHOSTBURROWEDMP: UnitTypeId.SWARMHOSTMP, 46 | UnitTypeId.THORAP: UnitTypeId.THOR, 47 | UnitTypeId.ULTRALISKBURROWED: UnitTypeId.ULTRALISK, 48 | UnitTypeId.VIKINGASSAULT: UnitTypeId.VIKINGFIGHTER, 49 | UnitTypeId.WARPPRISMPHASING: UnitTypeId.WARPPRISM, 50 | UnitTypeId.WIDOWMINEBURROWED: UnitTypeId.WIDOWMINE, 51 | UnitTypeId.ZERGLINGBURROWED: UnitTypeId.ZERGLING 52 | } 53 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/dicts/upgrade_researched_from.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED BY "generate_dicts_from_data_json.py" DO NOT CHANGE MANUALLY! 2 | # ANY CHANGE WILL BE OVERWRITTEN 3 | 4 | from typing import Dict 5 | 6 | from sc2.ids.unit_typeid import UnitTypeId 7 | from sc2.ids.upgrade_id import UpgradeId 8 | 9 | # from ..ids.buff_id import BuffId 10 | # from ..ids.effect_id import EffectId 11 | 12 | UPGRADE_RESEARCHED_FROM: Dict[UpgradeId, UnitTypeId] = { 13 | UpgradeId.ADEPTPIERCINGATTACK: UnitTypeId.TWILIGHTCOUNCIL, 14 | UpgradeId.ANABOLICSYNTHESIS: UnitTypeId.ULTRALISKCAVERN, 15 | UpgradeId.BANSHEECLOAK: UnitTypeId.STARPORTTECHLAB, 16 | UpgradeId.BANSHEESPEED: UnitTypeId.STARPORTTECHLAB, 17 | UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: UnitTypeId.FUSIONCORE, 18 | UpgradeId.BLINKTECH: UnitTypeId.TWILIGHTCOUNCIL, 19 | UpgradeId.BURROW: UnitTypeId.HATCHERY, 20 | UpgradeId.CENTRIFICALHOOKS: UnitTypeId.BANELINGNEST, 21 | UpgradeId.CHARGE: UnitTypeId.TWILIGHTCOUNCIL, 22 | UpgradeId.CHITINOUSPLATING: UnitTypeId.ULTRALISKCAVERN, 23 | UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: UnitTypeId.FACTORYTECHLAB, 24 | UpgradeId.DARKTEMPLARBLINKUPGRADE: UnitTypeId.DARKSHRINE, 25 | UpgradeId.DIGGINGCLAWS: UnitTypeId.LURKERDENMP, 26 | UpgradeId.DRILLCLAWS: UnitTypeId.FACTORYTECHLAB, 27 | UpgradeId.ENHANCEDSHOCKWAVES: UnitTypeId.GHOSTACADEMY, 28 | UpgradeId.EVOLVEGROOVEDSPINES: UnitTypeId.HYDRALISKDEN, 29 | UpgradeId.EVOLVEMUSCULARAUGMENTS: UnitTypeId.HYDRALISKDEN, 30 | UpgradeId.EXTENDEDTHERMALLANCE: UnitTypeId.ROBOTICSBAY, 31 | UpgradeId.GLIALRECONSTITUTION: UnitTypeId.ROACHWARREN, 32 | UpgradeId.GRAVITICDRIVE: UnitTypeId.ROBOTICSBAY, 33 | UpgradeId.HIGHCAPACITYBARRELS: UnitTypeId.FACTORYTECHLAB, 34 | UpgradeId.HISECAUTOTRACKING: UnitTypeId.ENGINEERINGBAY, 35 | UpgradeId.INFESTORENERGYUPGRADE: UnitTypeId.INFESTATIONPIT, 36 | UpgradeId.LIBERATORAGRANGEUPGRADE: UnitTypeId.FUSIONCORE, 37 | UpgradeId.LURKERRANGE: UnitTypeId.LURKERDENMP, 38 | UpgradeId.MEDIVACINCREASESPEEDBOOST: UnitTypeId.FUSIONCORE, 39 | UpgradeId.NEURALPARASITE: UnitTypeId.INFESTATIONPIT, 40 | UpgradeId.OBSERVERGRAVITICBOOSTER: UnitTypeId.ROBOTICSBAY, 41 | UpgradeId.OVERLORDSPEED: UnitTypeId.HATCHERY, 42 | UpgradeId.PERSONALCLOAKING: UnitTypeId.GHOSTACADEMY, 43 | UpgradeId.PHOENIXRANGEUPGRADE: UnitTypeId.FLEETBEACON, 44 | UpgradeId.PROTOSSAIRARMORSLEVEL1: UnitTypeId.CYBERNETICSCORE, 45 | UpgradeId.PROTOSSAIRARMORSLEVEL2: UnitTypeId.CYBERNETICSCORE, 46 | UpgradeId.PROTOSSAIRARMORSLEVEL3: UnitTypeId.CYBERNETICSCORE, 47 | UpgradeId.PROTOSSAIRWEAPONSLEVEL1: UnitTypeId.CYBERNETICSCORE, 48 | UpgradeId.PROTOSSAIRWEAPONSLEVEL2: UnitTypeId.CYBERNETICSCORE, 49 | UpgradeId.PROTOSSAIRWEAPONSLEVEL3: UnitTypeId.CYBERNETICSCORE, 50 | UpgradeId.PROTOSSGROUNDARMORSLEVEL1: UnitTypeId.FORGE, 51 | UpgradeId.PROTOSSGROUNDARMORSLEVEL2: UnitTypeId.FORGE, 52 | UpgradeId.PROTOSSGROUNDARMORSLEVEL3: UnitTypeId.FORGE, 53 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: UnitTypeId.FORGE, 54 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: UnitTypeId.FORGE, 55 | UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: UnitTypeId.FORGE, 56 | UpgradeId.PROTOSSSHIELDSLEVEL1: UnitTypeId.FORGE, 57 | UpgradeId.PROTOSSSHIELDSLEVEL2: UnitTypeId.FORGE, 58 | UpgradeId.PROTOSSSHIELDSLEVEL3: UnitTypeId.FORGE, 59 | UpgradeId.PSISTORMTECH: UnitTypeId.TEMPLARARCHIVE, 60 | UpgradeId.PUNISHERGRENADES: UnitTypeId.BARRACKSTECHLAB, 61 | UpgradeId.RAVENCORVIDREACTOR: UnitTypeId.STARPORTTECHLAB, 62 | UpgradeId.SHIELDWALL: UnitTypeId.BARRACKSTECHLAB, 63 | UpgradeId.SMARTSERVOS: UnitTypeId.FACTORYTECHLAB, 64 | UpgradeId.STIMPACK: UnitTypeId.BARRACKSTECHLAB, 65 | UpgradeId.TEMPESTGROUNDATTACKUPGRADE: UnitTypeId.FLEETBEACON, 66 | UpgradeId.TERRANBUILDINGARMOR: UnitTypeId.ENGINEERINGBAY, 67 | UpgradeId.TERRANINFANTRYARMORSLEVEL1: UnitTypeId.ENGINEERINGBAY, 68 | UpgradeId.TERRANINFANTRYARMORSLEVEL2: UnitTypeId.ENGINEERINGBAY, 69 | UpgradeId.TERRANINFANTRYARMORSLEVEL3: UnitTypeId.ENGINEERINGBAY, 70 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: UnitTypeId.ENGINEERINGBAY, 71 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: UnitTypeId.ENGINEERINGBAY, 72 | UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: UnitTypeId.ENGINEERINGBAY, 73 | UpgradeId.TERRANSHIPWEAPONSLEVEL1: UnitTypeId.ARMORY, 74 | UpgradeId.TERRANSHIPWEAPONSLEVEL2: UnitTypeId.ARMORY, 75 | UpgradeId.TERRANSHIPWEAPONSLEVEL3: UnitTypeId.ARMORY, 76 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: UnitTypeId.ARMORY, 77 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: UnitTypeId.ARMORY, 78 | UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: UnitTypeId.ARMORY, 79 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: UnitTypeId.ARMORY, 80 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: UnitTypeId.ARMORY, 81 | UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: UnitTypeId.ARMORY, 82 | UpgradeId.TUNNELINGCLAWS: UnitTypeId.ROACHWARREN, 83 | UpgradeId.VOIDRAYSPEEDUPGRADE: UnitTypeId.FLEETBEACON, 84 | UpgradeId.WARPGATERESEARCH: UnitTypeId.CYBERNETICSCORE, 85 | UpgradeId.ZERGFLYERARMORSLEVEL1: UnitTypeId.SPIRE, 86 | UpgradeId.ZERGFLYERARMORSLEVEL2: UnitTypeId.SPIRE, 87 | UpgradeId.ZERGFLYERARMORSLEVEL3: UnitTypeId.SPIRE, 88 | UpgradeId.ZERGFLYERWEAPONSLEVEL1: UnitTypeId.SPIRE, 89 | UpgradeId.ZERGFLYERWEAPONSLEVEL2: UnitTypeId.SPIRE, 90 | UpgradeId.ZERGFLYERWEAPONSLEVEL3: UnitTypeId.SPIRE, 91 | UpgradeId.ZERGGROUNDARMORSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 92 | UpgradeId.ZERGGROUNDARMORSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 93 | UpgradeId.ZERGGROUNDARMORSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, 94 | UpgradeId.ZERGLINGATTACKSPEED: UnitTypeId.SPAWNINGPOOL, 95 | UpgradeId.ZERGLINGMOVEMENTSPEED: UnitTypeId.SPAWNINGPOOL, 96 | UpgradeId.ZERGMELEEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 97 | UpgradeId.ZERGMELEEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 98 | UpgradeId.ZERGMELEEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER, 99 | UpgradeId.ZERGMISSILEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER, 100 | UpgradeId.ZERGMISSILEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER, 101 | UpgradeId.ZERGMISSILEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER 102 | } 103 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/expiring_dict.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import OrderedDict 4 | from threading import RLock 5 | from typing import TYPE_CHECKING, Any, Iterable, Union 6 | 7 | if TYPE_CHECKING: 8 | from sc2.bot_ai import BotAI 9 | 10 | 11 | class ExpiringDict(OrderedDict): 12 | """ 13 | An expiring dict that uses the bot.state.game_loop to only return items that are valid for a specific amount of time. 14 | 15 | Example usages:: 16 | 17 | async def on_step(iteration: int): 18 | # This dict will hold up to 10 items and only return values that have been added up to 20 frames ago 19 | my_dict = ExpiringDict(self, max_age_frames=20) 20 | if iteration == 0: 21 | # Add item 22 | my_dict["test"] = "something" 23 | if iteration == 2: 24 | # On default, one iteration is called every 8 frames 25 | if "test" in my_dict: 26 | print("test is in dict") 27 | if iteration == 20: 28 | if "test" not in my_dict: 29 | print("test is not anymore in dict") 30 | """ 31 | 32 | def __init__(self, bot: BotAI, max_age_frames: int = 1): 33 | assert max_age_frames >= -1 34 | assert bot 35 | 36 | OrderedDict.__init__(self) 37 | self.bot: BotAI = bot 38 | self.max_age: Union[int, float] = max_age_frames 39 | self.lock: RLock = RLock() 40 | 41 | @property 42 | def frame(self) -> int: 43 | return self.bot.state.game_loop 44 | 45 | def __contains__(self, key) -> bool: 46 | """ Return True if dict has key, else False, e.g. 'key in dict' """ 47 | with self.lock: 48 | if OrderedDict.__contains__(self, key): 49 | # Each item is a list of [value, frame time] 50 | item = OrderedDict.__getitem__(self, key) 51 | if self.frame - item[1] < self.max_age: 52 | return True 53 | del self[key] 54 | return False 55 | 56 | def __getitem__(self, key, with_age=False) -> Any: 57 | """ Return the item of the dict using d[key] """ 58 | with self.lock: 59 | # Each item is a list of [value, frame time] 60 | item = OrderedDict.__getitem__(self, key) 61 | if self.frame - item[1] < self.max_age: 62 | if with_age: 63 | return item[0], item[1] 64 | return item[0] 65 | OrderedDict.__delitem__(self, key) 66 | raise KeyError(key) 67 | 68 | def __setitem__(self, key, value): 69 | """ Set d[key] = value """ 70 | with self.lock: 71 | OrderedDict.__setitem__(self, key, (value, self.frame)) 72 | 73 | def __repr__(self): 74 | """ Printable version of the dict instead of getting memory adress """ 75 | print_list = [] 76 | with self.lock: 77 | for key, value in OrderedDict.items(self): 78 | if self.frame - value[1] < self.max_age: 79 | print_list.append(f"{repr(key)}: {repr(value)}") 80 | print_str = ", ".join(print_list) 81 | return f"ExpiringDict({print_str})" 82 | 83 | def __str__(self): 84 | return self.__repr__() 85 | 86 | def __iter__(self): 87 | """ Override 'for key in dict:' """ 88 | with self.lock: 89 | return self.keys() 90 | 91 | # TODO find a way to improve len 92 | def __len__(self): 93 | """Override len method as key value pairs aren't instantly being deleted, but only on __get__(item). 94 | This function is slow because it has to check if each element is not expired yet.""" 95 | with self.lock: 96 | count = 0 97 | for _ in self.values(): 98 | count += 1 99 | return count 100 | 101 | def pop(self, key, default=None, with_age=False): 102 | """ Return the item and remove it """ 103 | with self.lock: 104 | if OrderedDict.__contains__(self, key): 105 | item = OrderedDict.__getitem__(self, key) 106 | if self.frame - item[1] < self.max_age: 107 | del self[key] 108 | if with_age: 109 | return item[0], item[1] 110 | return item[0] 111 | del self[key] 112 | if default is None: 113 | raise KeyError(key) 114 | if with_age: 115 | return default, self.frame 116 | return default 117 | 118 | def get(self, key, default=None, with_age=False): 119 | """ Return the value for key if key is in dict, else default """ 120 | with self.lock: 121 | if OrderedDict.__contains__(self, key): 122 | item = OrderedDict.__getitem__(self, key) 123 | if self.frame - item[1] < self.max_age: 124 | if with_age: 125 | return item[0], item[1] 126 | return item[0] 127 | if default is None: 128 | raise KeyError(key) 129 | if with_age: 130 | return default, self.frame 131 | return None 132 | return None 133 | 134 | def update(self, other_dict: dict): 135 | with self.lock: 136 | for key, value in other_dict.items(): 137 | self[key] = value 138 | 139 | def items(self) -> Iterable: 140 | """ Return iterator of zipped list [keys, values] """ 141 | with self.lock: 142 | for key, value in OrderedDict.items(self): 143 | if self.frame - value[1] < self.max_age: 144 | yield key, value[0] 145 | 146 | def keys(self) -> Iterable: 147 | """ Return iterator of keys """ 148 | with self.lock: 149 | for key, value in OrderedDict.items(self): 150 | if self.frame - value[1] < self.max_age: 151 | yield key 152 | 153 | def values(self) -> Iterable: 154 | """ Return iterator of values """ 155 | with self.lock: 156 | for value in OrderedDict.values(self): 157 | if self.frame - value[1] < self.max_age: 158 | yield value[0] 159 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/generate_ids.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0212 2 | import importlib 3 | import json 4 | import platform 5 | import subprocess 6 | import sys 7 | from pathlib import Path 8 | 9 | from loguru import logger 10 | 11 | from sc2.game_data import AbilityData, GameData, UnitTypeData, UpgradeData 12 | from sc2.ids.ability_id import AbilityId 13 | 14 | try: 15 | from sc2.ids.id_version import ID_VERSION_STRING 16 | except ImportError: 17 | ID_VERSION_STRING = "4.11.4.78285" 18 | 19 | 20 | class IdGenerator: 21 | 22 | def __init__(self, game_data: GameData = None, game_version: str = None, verbose: bool = False): 23 | self.game_data: GameData = game_data 24 | self.game_version = game_version 25 | self.verbose = verbose 26 | 27 | self.HEADER = f'# DO NOT EDIT!\n# This file was automatically generated by "{Path(__file__).name}"\n' 28 | 29 | self.PF = platform.system() 30 | 31 | self.HOME_DIR = str(Path.home()) 32 | self.DATA_JSON = { 33 | "Darwin": self.HOME_DIR + "/Library/Application Support/Blizzard/StarCraft II/stableid.json", 34 | "Windows": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", 35 | "Linux": self.HOME_DIR + "/Documents/StarCraft II/stableid.json", 36 | } 37 | 38 | self.ENUM_TRANSLATE = { 39 | "Units": "UnitTypeId", 40 | "Abilities": "AbilityId", 41 | "Upgrades": "UpgradeId", 42 | "Buffs": "BuffId", 43 | "Effects": "EffectId", 44 | } 45 | 46 | self.FILE_TRANSLATE = { 47 | "Units": "unit_typeid", 48 | "Abilities": "ability_id", 49 | "Upgrades": "upgrade_id", 50 | "Buffs": "buff_id", 51 | "Effects": "effect_id", 52 | } 53 | 54 | @staticmethod 55 | def make_key(key): 56 | if key[0].isdigit(): 57 | key = "_" + key 58 | # In patch 5.0, the key has "@" character in it which is not possible with python enums 59 | return key.upper().replace(" ", "_").replace("@", "") 60 | 61 | def parse_data(self, data): 62 | # for d in data: # Units, Abilities, Upgrades, Buffs, Effects 63 | 64 | units = self.parse_simple("Units", data) 65 | upgrades = self.parse_simple("Upgrades", data) 66 | effects = self.parse_simple("Effects", data) 67 | buffs = self.parse_simple("Buffs", data) 68 | 69 | abilities = {} 70 | for v in data["Abilities"]: 71 | key = v["buttonname"] 72 | remapid = v.get("remapid") 73 | 74 | if (not key) and (remapid is None): 75 | assert v["buttonname"] == "" 76 | continue 77 | 78 | if not key: 79 | if v["friendlyname"] != "": 80 | key = v["friendlyname"] 81 | else: 82 | sys.exit(f"Not mapped: {v !r}") 83 | 84 | key = key.upper().replace(" ", "_").replace("@", "") 85 | 86 | if "name" in v: 87 | key = f'{v["name"].upper().replace(" ", "_")}_{key}' 88 | 89 | if "friendlyname" in v: 90 | key = v["friendlyname"].upper().replace(" ", "_") 91 | 92 | if key[0].isdigit(): 93 | key = "_" + key 94 | 95 | if key in abilities and v["index"] == 0: 96 | logger.info(f"{key} has value 0 and id {v['id']}, overwriting {key}: {abilities[key]}") 97 | # Commented out to try to fix: 3670 is not a valid AbilityId 98 | abilities[key] = v["id"] 99 | elif key in abilities: 100 | logger.info(f"{key} has appeared a second time with id={v['id']}") 101 | else: 102 | abilities[key] = v["id"] 103 | 104 | abilities["SMART"] = 1 105 | 106 | enums = {} 107 | enums["Units"] = units 108 | enums["Abilities"] = abilities 109 | enums["Upgrades"] = upgrades 110 | enums["Buffs"] = buffs 111 | enums["Effects"] = effects 112 | 113 | return enums 114 | 115 | def parse_simple(self, d, data): 116 | units = {} 117 | for v in data[d]: 118 | key = v["name"] 119 | 120 | if not key: 121 | continue 122 | key_to_insert = self.make_key(key) 123 | if key_to_insert in units: 124 | index = 2 125 | tmp = f"{key_to_insert}_{index}" 126 | while tmp in units: 127 | index += 1 128 | tmp = f"{key_to_insert}_{index}" 129 | key_to_insert = tmp 130 | units[key_to_insert] = v["id"] 131 | 132 | return units 133 | 134 | def generate_python_code(self, enums): 135 | assert {"Units", "Abilities", "Upgrades", "Buffs", "Effects"} <= enums.keys() 136 | 137 | sc2dir = Path(__file__).parent 138 | idsdir = sc2dir / "ids" 139 | idsdir.mkdir(exist_ok=True) 140 | 141 | with (idsdir / "__init__.py").open("w") as f: 142 | initstring = f"__all__ = {[n.lower() for n in self.FILE_TRANSLATE.values()] !r}\n".replace("'", '"') 143 | f.write("\n".join([self.HEADER, initstring])) 144 | 145 | for name, body in enums.items(): 146 | class_name = self.ENUM_TRANSLATE[name] 147 | 148 | code = [self.HEADER, "import enum", "\n", f"class {class_name}(enum.Enum):"] 149 | 150 | for key, value in sorted(body.items(), key=lambda p: p[1]): 151 | code.append(f" {key} = {value}") 152 | 153 | # Add repr function to more easily dump enums to dict 154 | code += f""" 155 | def __repr__(self) -> str: 156 | return f"{class_name}.{{self.name}}" 157 | """.split("\n") 158 | 159 | # Add missing ids function to not make the game crash when unknown BuffId was detected 160 | if class_name == "BuffId": 161 | code += f""" 162 | @classmethod 163 | def _missing_(cls, value: int) -> "{class_name}": 164 | return cls.NULL 165 | """.split("\n") 166 | 167 | code += f""" 168 | for item in {class_name}: 169 | globals()[item.name] = item 170 | """.split("\n") 171 | 172 | ids_file_path = (idsdir / self.FILE_TRANSLATE[name]).with_suffix(".py") 173 | with ids_file_path.open("w") as f: 174 | f.write("\n".join(code)) 175 | 176 | # Apply formatting 177 | try: 178 | subprocess.run(["poetry", "run", "yapf", ids_file_path, "-i"], check=True) 179 | except FileNotFoundError: 180 | logger.info( 181 | f"Yapf is not installed. Please use 'pip install yapf' to install yapf formatter.\nCould not autoformat file {ids_file_path}" 182 | ) 183 | 184 | if self.game_version is not None: 185 | version_path = Path(__file__).parent / "ids" / "id_version.py" 186 | with open(version_path, "w") as f: 187 | f.write(f'ID_VERSION_STRING = "{self.game_version}"\n') 188 | 189 | def update_ids_from_stableid_json(self): 190 | if self.game_version is None or ID_VERSION_STRING is None or ID_VERSION_STRING != self.game_version: 191 | if self.verbose and self.game_version is not None and ID_VERSION_STRING is not None: 192 | logger.info( 193 | f"Game version is different (Old: {self.game_version}, new: {ID_VERSION_STRING}. Updating ids to match game version" 194 | ) 195 | stable_id_path = Path(self.DATA_JSON[self.PF]) 196 | assert stable_id_path.is_file(), f"stable_id.json was not found at path \"{stable_id_path}\"" 197 | with stable_id_path.open(encoding="utf-8") as data_file: 198 | data = json.loads(data_file.read()) 199 | self.generate_python_code(self.parse_data(data)) 200 | 201 | # Update game_data if this is a live game 202 | if self.game_data is not None: 203 | self.reimport_ids() 204 | self.update_game_data() 205 | 206 | @staticmethod 207 | def reimport_ids(): 208 | 209 | # Reload the newly written "id" files 210 | # TODO This only re-imports modules, but if they haven't been imported, it will yield an error 211 | importlib.reload(sys.modules["sc2.ids.ability_id"]) 212 | 213 | importlib.reload(sys.modules["sc2.ids.unit_typeid"]) 214 | 215 | importlib.reload(sys.modules["sc2.ids.upgrade_id"]) 216 | 217 | importlib.reload(sys.modules["sc2.ids.effect_id"]) 218 | 219 | importlib.reload(sys.modules["sc2.ids.buff_id"]) 220 | 221 | # importlib.reload(sys.modules["sc2.ids.id_version"]) 222 | 223 | importlib.reload(sys.modules["sc2.constants"]) 224 | 225 | def update_game_data(self): 226 | """Re-generate the dicts from self.game_data. 227 | This should be done after the ids have been reimported.""" 228 | ids = set(a.value for a in AbilityId if a.value != 0) 229 | self.game_data.abilities = { 230 | a.ability_id: AbilityData(self.game_data, a) 231 | for a in self.game_data._proto.abilities if a.ability_id in ids 232 | } 233 | # self.game_data.abilities = { 234 | # a.ability_id: AbilityData(self.game_data, a) for a in self.game_data._proto.abilities 235 | # } 236 | self.game_data.units = { 237 | u.unit_id: UnitTypeData(self.game_data, u) 238 | for u in self.game_data._proto.units if u.available 239 | } 240 | self.game_data.upgrades = {u.upgrade_id: UpgradeData(self.game_data, u) for u in self.game_data._proto.upgrades} 241 | 242 | 243 | if __name__ == "__main__": 244 | updater = IdGenerator() 245 | updater.update_ids_from_stableid_json() 246 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/ids/__init__.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_ids.py" 3 | 4 | __all__ = ["unit_typeid", "ability_id", "upgrade_id", "buff_id", "effect_id"] 5 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/ids/effect_id.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file was automatically generated by "generate_ids.py" 3 | 4 | import enum 5 | 6 | 7 | class EffectId(enum.Enum): 8 | NULL = 0 9 | PSISTORMPERSISTENT = 1 10 | GUARDIANSHIELDPERSISTENT = 2 11 | TEMPORALFIELDGROWINGBUBBLECREATEPERSISTENT = 3 12 | TEMPORALFIELDAFTERBUBBLECREATEPERSISTENT = 4 13 | THERMALLANCESFORWARD = 5 14 | SCANNERSWEEP = 6 15 | NUKEPERSISTENT = 7 16 | LIBERATORTARGETMORPHDELAYPERSISTENT = 8 17 | LIBERATORTARGETMORPHPERSISTENT = 9 18 | BLINDINGCLOUDCP = 10 19 | RAVAGERCORROSIVEBILECP = 11 20 | LURKERMP = 12 21 | 22 | def __repr__(self) -> str: 23 | return f"EffectId.{self.name}" 24 | 25 | 26 | for item in EffectId: 27 | globals()[item.name] = item 28 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/ids/id_version.py: -------------------------------------------------------------------------------- 1 | ID_VERSION_STRING = "4.11.4.78285" 2 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/maps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from loguru import logger 6 | 7 | from sc2.paths import Paths 8 | 9 | 10 | def get(name: str) -> Map: 11 | # Iterate through 2 folder depths 12 | for map_dir in (p for p in Paths.MAPS.iterdir()): 13 | if map_dir.is_dir(): 14 | for map_file in (p for p in map_dir.iterdir()): 15 | if Map.matches_target_map_name(map_file, name): 16 | return Map(map_file) 17 | elif Map.matches_target_map_name(map_dir, name): 18 | return Map(map_dir) 19 | 20 | raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".") 21 | 22 | 23 | class Map: 24 | 25 | def __init__(self, path: Path): 26 | self.path = path 27 | 28 | if self.path.is_absolute(): 29 | try: 30 | self.relative_path = self.path.relative_to(Paths.MAPS) 31 | except ValueError: # path not relative to basedir 32 | logger.warning(f"Using absolute path: {self.path}") 33 | self.relative_path = self.path 34 | else: 35 | self.relative_path = self.path 36 | 37 | @property 38 | def name(self): 39 | return self.path.stem 40 | 41 | @property 42 | def data(self): 43 | with open(self.path, "rb") as f: 44 | return f.read() 45 | 46 | def __repr__(self): 47 | return f"Map({self.path})" 48 | 49 | @classmethod 50 | def is_map_file(cls, file: Path) -> bool: 51 | return file.is_file() and file.suffix == ".SC2Map" 52 | 53 | @classmethod 54 | def matches_target_map_name(cls, file: Path, name: str) -> bool: 55 | return cls.is_map_file(file) and file.stem == name 56 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/observer_ai.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class is very experimental and probably not up to date and needs to be refurbished. 3 | If it works, you can watch replays with it. 4 | """ 5 | 6 | # pylint: disable=W0201,W0212 7 | from __future__ import annotations 8 | 9 | from typing import TYPE_CHECKING, List, Union 10 | 11 | from sc2.bot_ai_internal import BotAIInternal 12 | from sc2.data import Alert, Result 13 | from sc2.game_data import GameData 14 | from sc2.ids.ability_id import AbilityId 15 | from sc2.ids.upgrade_id import UpgradeId 16 | from sc2.position import Point2 17 | from sc2.unit import Unit 18 | from sc2.units import Units 19 | 20 | if TYPE_CHECKING: 21 | from sc2.client import Client 22 | from sc2.game_info import GameInfo 23 | 24 | 25 | class ObserverAI(BotAIInternal): 26 | """Base class for bots.""" 27 | 28 | @property 29 | def time(self) -> float: 30 | """ Returns time in seconds, assumes the game is played on 'faster' """ 31 | return self.state.game_loop / 22.4 # / (1/1.4) * (1/16) 32 | 33 | @property 34 | def time_formatted(self) -> str: 35 | """ Returns time as string in min:sec format """ 36 | t = self.time 37 | return f"{int(t // 60):02}:{int(t % 60):02}" 38 | 39 | @property 40 | def game_info(self) -> GameInfo: 41 | """ See game_info.py """ 42 | return self._game_info 43 | 44 | @property 45 | def game_data(self) -> GameData: 46 | """ See game_data.py """ 47 | return self._game_data 48 | 49 | @property 50 | def client(self) -> Client: 51 | """ See client.py """ 52 | return self._client 53 | 54 | def alert(self, alert_code: Alert) -> bool: 55 | """ 56 | Check if alert is triggered in the current step. 57 | Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702 58 | 59 | Example use: 60 | 61 | from sc2.data import Alert 62 | if self.alert(Alert.AddOnComplete): 63 | print("Addon Complete") 64 | 65 | Alert codes:: 66 | 67 | AlertError 68 | AddOnComplete 69 | BuildingComplete 70 | BuildingUnderAttack 71 | LarvaHatched 72 | MergeComplete 73 | MineralsExhausted 74 | MorphComplete 75 | MothershipComplete 76 | MULEExpired 77 | NuclearLaunchDetected 78 | NukeComplete 79 | NydusWormDetected 80 | ResearchComplete 81 | TrainError 82 | TrainUnitComplete 83 | TrainWorkerComplete 84 | TransformationComplete 85 | UnitUnderAttack 86 | UpgradeComplete 87 | VespeneExhausted 88 | WarpInComplete 89 | 90 | :param alert_code: 91 | """ 92 | assert isinstance(alert_code, Alert), f"alert_code {alert_code} is no Alert" 93 | return alert_code.value in self.state.alerts 94 | 95 | @property 96 | def start_location(self) -> Point2: 97 | """ 98 | Returns the spawn location of the bot, using the position of the first created townhall. 99 | This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start. 100 | """ 101 | return self.game_info.player_start_location 102 | 103 | @property 104 | def enemy_start_locations(self) -> List[Point2]: 105 | """Possible start locations for enemies.""" 106 | return self.game_info.start_locations 107 | 108 | async def get_available_abilities( 109 | self, units: Union[List[Unit], Units], ignore_resource_requirements: bool = False 110 | ) -> List[List[AbilityId]]: 111 | """Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched. 112 | 113 | Examples:: 114 | 115 | units_abilities = await self.get_available_abilities(self.units) 116 | 117 | or:: 118 | 119 | units_abilities = await self.get_available_abilities([self.units.random]) 120 | 121 | :param units: 122 | :param ignore_resource_requirements:""" 123 | return await self.client.query_available_abilities(units, ignore_resource_requirements) 124 | 125 | async def on_unit_destroyed(self, unit_tag: int): 126 | """ 127 | Override this in your bot class. 128 | This will event will be called when a unit (or structure, friendly or enemy) dies. 129 | For enemy units, this only works if the enemy unit was in vision on death. 130 | 131 | :param unit_tag: 132 | """ 133 | 134 | async def on_unit_created(self, unit: Unit): 135 | """Override this in your bot class. This function is called when a unit is created. 136 | 137 | :param unit:""" 138 | 139 | async def on_building_construction_started(self, unit: Unit): 140 | """ 141 | Override this in your bot class. 142 | This function is called when a building construction has started. 143 | 144 | :param unit: 145 | """ 146 | 147 | async def on_building_construction_complete(self, unit: Unit): 148 | """ 149 | Override this in your bot class. This function is called when a building 150 | construction is completed. 151 | 152 | :param unit: 153 | """ 154 | 155 | async def on_upgrade_complete(self, upgrade: UpgradeId): 156 | """ 157 | Override this in your bot class. This function is called with the upgrade id of an upgrade that was not finished last step and is now. 158 | 159 | :param upgrade: 160 | """ 161 | 162 | async def on_start(self): 163 | """ 164 | Override this in your bot class. This function is called after "on_start". 165 | At this point, game_data, game_info and the first iteration of game_state (self.state) are available. 166 | """ 167 | 168 | async def on_step(self, iteration: int): 169 | """ 170 | You need to implement this function! 171 | Override this in your bot class. 172 | This function is called on every game step (looped in realtime mode). 173 | 174 | :param iteration: 175 | """ 176 | raise NotImplementedError 177 | 178 | async def on_end(self, game_result: Result): 179 | """Override this in your bot class. This function is called at the end of a game. 180 | 181 | :param game_result:""" 182 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import re 4 | import sys 5 | from contextlib import suppress 6 | from pathlib import Path 7 | 8 | from loguru import logger 9 | 10 | from sc2 import wsl 11 | 12 | BASEDIR = { 13 | "Windows": "C:/Program Files (x86)/StarCraft II", 14 | "WSL1": "/mnt/c/Program Files (x86)/StarCraft II", 15 | "WSL2": "/mnt/c/Program Files (x86)/StarCraft II", 16 | "Darwin": "/Applications/StarCraft II", 17 | "Linux": "~/StarCraftII", 18 | "WineLinux": "~/.wine/drive_c/Program Files (x86)/StarCraft II", 19 | } 20 | 21 | USERPATH = { 22 | "Windows": "Documents\\StarCraft II\\ExecuteInfo.txt", 23 | "WSL1": "Documents/StarCraft II/ExecuteInfo.txt", 24 | "WSL2": "Documents/StarCraft II/ExecuteInfo.txt", 25 | "Darwin": "Library/Application Support/Blizzard/StarCraft II/ExecuteInfo.txt", 26 | "Linux": None, 27 | "WineLinux": None, 28 | } 29 | 30 | BINPATH = { 31 | "Windows": "SC2_x64.exe", 32 | "WSL1": "SC2_x64.exe", 33 | "WSL2": "SC2_x64.exe", 34 | "Darwin": "SC2.app/Contents/MacOS/SC2", 35 | "Linux": "SC2_x64", 36 | "WineLinux": "SC2_x64.exe", 37 | } 38 | 39 | CWD = { 40 | "Windows": "Support64", 41 | "WSL1": "Support64", 42 | "WSL2": "Support64", 43 | "Darwin": None, 44 | "Linux": None, 45 | "WineLinux": "Support64", 46 | } 47 | 48 | 49 | def platform_detect(): 50 | pf = os.environ.get("SC2PF", platform.system()) 51 | if pf == "Linux": 52 | return wsl.detect() or pf 53 | return pf 54 | 55 | 56 | PF = platform_detect() 57 | 58 | 59 | def get_home(): 60 | """Get home directory of user, using Windows home directory for WSL.""" 61 | if PF in {"WSL1", "WSL2"}: 62 | return wsl.get_wsl_home() or Path.home().expanduser() 63 | return Path.home().expanduser() 64 | 65 | 66 | def get_user_sc2_install(): 67 | """Attempts to find a user's SC2 install if their OS has ExecuteInfo.txt""" 68 | if USERPATH[PF]: 69 | einfo = str(get_home() / Path(USERPATH[PF])) 70 | if os.path.isfile(einfo): 71 | with open(einfo) as f: 72 | content = f.read() 73 | if content: 74 | base = re.search(r" = (.*)Versions", content).group(1) 75 | if PF in {"WSL1", "WSL2"}: 76 | base = str(wsl.win_path_to_wsl_path(base)) 77 | 78 | if os.path.exists(base): 79 | return base 80 | return None 81 | 82 | 83 | def get_env(): 84 | # TODO: Linux env conf from: https://github.com/deepmind/pysc2/blob/master/pysc2/run_configs/platforms.py 85 | return None 86 | 87 | 88 | def get_runner_args(cwd): 89 | if "WINE" in os.environ: 90 | runner_file = Path(os.environ.get("WINE")) 91 | runner_file = runner_file if runner_file.is_file() else runner_file / "wine" 92 | """ 93 | TODO Is converting linux path really necessary? 94 | That would convert 95 | '/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/Support64' 96 | to 97 | 'Z:\\home\\burny\\Games\\battlenet\\drive_c\\Program Files (x86)\\StarCraft II\\Support64' 98 | """ 99 | return [runner_file, "start", "/d", cwd, "/unix"] 100 | return [] 101 | 102 | 103 | def latest_executeble(versions_dir, base_build=None): 104 | latest = None 105 | 106 | if base_build is not None: 107 | with suppress(ValueError): 108 | latest = ( 109 | int(base_build[4:]), 110 | max(p for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith(str(base_build))), 111 | ) 112 | 113 | if base_build is None or latest is None: 114 | latest = max((int(p.name[4:]), p) for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith("Base")) 115 | 116 | version, path = latest 117 | 118 | if version < 55958: 119 | logger.critical("Your SC2 binary is too old. Upgrade to 3.16.1 or newer.") 120 | sys.exit(1) 121 | return path / BINPATH[PF] 122 | 123 | 124 | class _MetaPaths(type): 125 | """"Lazily loads paths to allow importing the library even if SC2 isn't installed.""" 126 | 127 | # pylint: disable=C0203 128 | def __setup(self): 129 | if PF not in BASEDIR: 130 | logger.critical(f"Unsupported platform '{PF}'") 131 | sys.exit(1) 132 | 133 | try: 134 | base = os.environ.get("SC2PATH") or get_user_sc2_install() or BASEDIR[PF] 135 | self.BASE = Path(base).expanduser() 136 | self.EXECUTABLE = latest_executeble(self.BASE / "Versions") 137 | self.CWD = self.BASE / CWD[PF] if CWD[PF] else None 138 | 139 | self.REPLAYS = self.BASE / "Replays" 140 | 141 | if (self.BASE / "maps").exists(): 142 | self.MAPS = self.BASE / "maps" 143 | else: 144 | self.MAPS = self.BASE / "Maps" 145 | except FileNotFoundError as e: 146 | logger.critical(f"SC2 installation not found: File '{e.filename}' does not exist.") 147 | sys.exit(1) 148 | 149 | # pylint: disable=C0203 150 | def __getattr__(self, attr): 151 | # pylint: disable=E1120 152 | self.__setup() 153 | return getattr(self, attr) 154 | 155 | 156 | class Paths(metaclass=_MetaPaths): 157 | """Paths for SC2 folders, lazily loaded using the above metaclass.""" 158 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/pixel_map.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Callable, FrozenSet, List, Set, Tuple, Union 3 | 4 | import numpy as np 5 | 6 | from sc2.position import Point2 7 | 8 | 9 | class PixelMap: 10 | 11 | def __init__(self, proto, in_bits: bool = False): 12 | """ 13 | :param proto: 14 | :param in_bits: 15 | """ 16 | self._proto = proto 17 | # Used for copying pixelmaps 18 | self._in_bits: bool = in_bits 19 | 20 | assert self.width * self.height == (8 if in_bits else 1) * len( 21 | self._proto.data 22 | ), f"{self.width * self.height} {(8 if in_bits else 1)*len(self._proto.data)}" 23 | buffer_data = np.frombuffer(self._proto.data, dtype=np.uint8) 24 | if in_bits: 25 | buffer_data = np.unpackbits(buffer_data) 26 | self.data_numpy = buffer_data.reshape(self._proto.size.y, self._proto.size.x) 27 | 28 | @property 29 | def width(self) -> int: 30 | return self._proto.size.x 31 | 32 | @property 33 | def height(self) -> int: 34 | return self._proto.size.y 35 | 36 | @property 37 | def bits_per_pixel(self) -> int: 38 | return self._proto.bits_per_pixel 39 | 40 | @property 41 | def bytes_per_pixel(self) -> int: 42 | return self._proto.bits_per_pixel // 8 43 | 44 | def __getitem__(self, pos: Tuple[int, int]) -> int: 45 | """ Example usage: is_pathable = self._game_info.pathing_grid[Point2((20, 20))] != 0 """ 46 | assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" 47 | assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" 48 | return int(self.data_numpy[pos[1], pos[0]]) 49 | 50 | def __setitem__(self, pos: Tuple[int, int], value: int): 51 | """ Example usage: self._game_info.pathing_grid[Point2((20, 20))] = 255 """ 52 | assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" 53 | assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" 54 | assert ( 55 | 0 <= value <= 254 * self._in_bits + 1 56 | ), f"value is {value}, it should be between 0 and {254 * self._in_bits + 1}" 57 | assert isinstance(value, int), f"value is of type {type(value)}, it should be an integer" 58 | self.data_numpy[pos[1], pos[0]] = value 59 | 60 | def is_set(self, p: Tuple[int, int]) -> bool: 61 | return self[p] != 0 62 | 63 | def is_empty(self, p: Tuple[int, int]) -> bool: 64 | return not self.is_set(p) 65 | 66 | def copy(self) -> "PixelMap": 67 | return PixelMap(self._proto, in_bits=self._in_bits) 68 | 69 | def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> Set[Point2]: 70 | nodes: Set[Point2] = set() 71 | queue: List[Point2] = [start_point] 72 | 73 | while queue: 74 | x, y = queue.pop() 75 | 76 | if not (0 <= x < self.width and 0 <= y < self.height): 77 | continue 78 | 79 | if Point2((x, y)) in nodes: 80 | continue 81 | 82 | if pred(self[x, y]): 83 | nodes.add(Point2((x, y))) 84 | queue += [Point2((x + a, y + b)) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)] 85 | return nodes 86 | 87 | def flood_fill_all(self, pred: Callable[[int], bool]) -> Set[FrozenSet[Point2]]: 88 | groups: Set[FrozenSet[Point2]] = set() 89 | 90 | for x in range(self.width): 91 | for y in range(self.height): 92 | if any((x, y) in g for g in groups): 93 | continue 94 | 95 | if pred(self[x, y]): 96 | groups.add(frozenset(self.flood_fill(Point2((x, y)), pred))) 97 | 98 | return groups 99 | 100 | def print(self, wide: bool = False) -> None: 101 | for y in range(self.height): 102 | for x in range(self.width): 103 | print("#" if self.is_set((x, y)) else " ", end=(" " if wide else "")) 104 | print("") 105 | 106 | def save_image(self, filename: Union[str, Path]): 107 | data = [(0, 0, self[x, y]) for y in range(self.height) for x in range(self.width)] 108 | # pylint: disable=C0415 109 | from PIL import Image 110 | 111 | im = Image.new("RGB", (self.width, self.height)) 112 | im.putdata(data) # type: ignore 113 | im.save(filename) 114 | 115 | def plot(self): 116 | # pylint: disable=C0415 117 | import matplotlib.pyplot as plt 118 | 119 | plt.imshow(self.data_numpy, origin="lower") 120 | plt.show() 121 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/player.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from pathlib import Path 3 | from typing import List, Union 4 | 5 | from sc2.bot_ai import BotAI 6 | from sc2.data import AIBuild, Difficulty, PlayerType, Race 7 | 8 | 9 | class AbstractPlayer(ABC): 10 | 11 | def __init__( 12 | self, 13 | p_type: PlayerType, 14 | race: Race = None, 15 | name: str = None, 16 | difficulty=None, 17 | ai_build=None, 18 | fullscreen=False 19 | ): 20 | assert isinstance(p_type, PlayerType), f"p_type is of type {type(p_type)}" 21 | assert name is None or isinstance(name, str), f"name is of type {type(name)}" 22 | 23 | self.name = name 24 | self.type = p_type 25 | self.fullscreen = fullscreen 26 | if race is not None: 27 | self.race = race 28 | if p_type == PlayerType.Computer: 29 | assert isinstance(difficulty, Difficulty), f"difficulty is of type {type(difficulty)}" 30 | # Workaround, proto information does not carry ai_build info 31 | # We cant set that in the Player classmethod 32 | assert ai_build is None or isinstance(ai_build, AIBuild), f"ai_build is of type {type(ai_build)}" 33 | self.difficulty = difficulty 34 | self.ai_build = ai_build 35 | 36 | elif p_type == PlayerType.Observer: 37 | assert race is None 38 | assert difficulty is None 39 | assert ai_build is None 40 | 41 | else: 42 | assert isinstance(race, Race), f"race is of type {type(race)}" 43 | assert difficulty is None 44 | assert ai_build is None 45 | 46 | @property 47 | def needs_sc2(self): 48 | return not isinstance(self, Computer) 49 | 50 | 51 | class Human(AbstractPlayer): 52 | 53 | def __init__(self, race, name=None, fullscreen=False): 54 | super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) 55 | 56 | def __str__(self): 57 | if self.name is not None: 58 | return f"Human({self.race._name_}, name={self.name !r})" 59 | return f"Human({self.race._name_})" 60 | 61 | 62 | class Bot(AbstractPlayer): 63 | 64 | def __init__(self, race, ai, name=None, fullscreen=False): 65 | """ 66 | AI can be None if this player object is just used to inform the 67 | server about player types. 68 | """ 69 | assert isinstance(ai, BotAI) or ai is None, f"ai is of type {type(ai)}, inherit BotAI from bot_ai.py" 70 | super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) 71 | self.ai = ai 72 | 73 | def __str__(self): 74 | if self.name is not None: 75 | return f"Bot {self.ai.__class__.__name__}({self.race._name_}), name={self.name !r})" 76 | return f"Bot {self.ai.__class__.__name__}({self.race._name_})" 77 | 78 | 79 | class Computer(AbstractPlayer): 80 | 81 | def __init__(self, race, difficulty=Difficulty.Easy, ai_build=AIBuild.RandomBuild): 82 | super().__init__(PlayerType.Computer, race, difficulty=difficulty, ai_build=ai_build) 83 | 84 | def __str__(self): 85 | return f"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})" 86 | 87 | 88 | class Observer(AbstractPlayer): 89 | 90 | def __init__(self): 91 | super().__init__(PlayerType.Observer) 92 | 93 | def __str__(self): 94 | return "Observer" 95 | 96 | 97 | class Player(AbstractPlayer): 98 | 99 | def __init__(self, player_id, p_type, requested_race, difficulty=None, actual_race=None, name=None, ai_build=None): 100 | super().__init__(p_type, requested_race, difficulty=difficulty, name=name, ai_build=ai_build) 101 | self.id: int = player_id 102 | self.actual_race: Race = actual_race 103 | 104 | @classmethod 105 | def from_proto(cls, proto): 106 | if PlayerType(proto.type) == PlayerType.Observer: 107 | return cls(proto.player_id, PlayerType(proto.type), None, None, None) 108 | return cls( 109 | proto.player_id, 110 | PlayerType(proto.type), 111 | Race(proto.race_requested), 112 | Difficulty(proto.difficulty) if proto.HasField("difficulty") else None, 113 | Race(proto.race_actual) if proto.HasField("race_actual") else None, 114 | proto.player_name if proto.HasField("player_name") else None, 115 | ) 116 | 117 | 118 | class BotProcess(AbstractPlayer): 119 | """ 120 | Class for handling bots launched externally, including non-python bots. 121 | Default parameters comply with sc2ai and aiarena ladders. 122 | 123 | :param path: the executable file's path 124 | :param launch_list: list of strings that launches the bot e.g. ["python", "run.py"] or ["run.exe"] 125 | :param race: bot's race 126 | :param name: bot's name 127 | :param sc2port_arg: the accepted argument name for the port of the sc2 instance to listen to 128 | :param hostaddress_arg: the accepted argument name for the address of the sc2 instance to listen to 129 | :param match_arg: the accepted argument name for the starting port to generate a portconfig from 130 | :param realtime_arg: the accepted argument name for specifying realtime 131 | :param other_args: anything else that is needed 132 | 133 | e.g. to call a bot capable of running on the bot ladders: 134 | BotProcess(os.getcwd(), "python run.py", Race.Terran, "INnoVation") 135 | """ 136 | 137 | def __init__( 138 | self, 139 | path: Union[str, Path], 140 | launch_list: List[str], 141 | race: Race, 142 | name=None, 143 | sc2port_arg="--GamePort", 144 | hostaddress_arg="--LadderServer", 145 | match_arg="--StartPort", 146 | realtime_arg="--RealTime", 147 | other_args: str = None, 148 | stdout: str = None, 149 | ): 150 | super().__init__(PlayerType.Participant, race, name=name) 151 | assert Path(path).exists() 152 | self.path = path 153 | self.launch_list = launch_list 154 | self.sc2port_arg = sc2port_arg 155 | self.match_arg = match_arg 156 | self.hostaddress_arg = hostaddress_arg 157 | self.realtime_arg = realtime_arg 158 | self.other_args = other_args 159 | self.stdout = stdout 160 | 161 | def __repr__(self): 162 | if self.name is not None: 163 | return f"Bot {self.name}({self.race.name} from {self.launch_list})" 164 | return f"Bot({self.race.name} from {self.launch_list})" 165 | 166 | def cmd_line(self, 167 | sc2port: Union[int, str], 168 | matchport: Union[int, str], 169 | hostaddress: str, 170 | realtime: bool = False) -> List[str]: 171 | """ 172 | 173 | :param sc2port: the port that the launched sc2 instance listens to 174 | :param matchport: some starting port that both bots use to generate identical portconfigs. 175 | Note: This will not be sent if playing vs computer 176 | :param hostaddress: the address the sc2 instances used 177 | :param realtime: 1 or 0, indicating whether the match is played in realtime or not 178 | :return: string that will be used to start the bot's process 179 | """ 180 | cmd_line = [ 181 | *self.launch_list, 182 | self.sc2port_arg, 183 | str(sc2port), 184 | self.hostaddress_arg, 185 | hostaddress, 186 | ] 187 | if matchport is not None: 188 | cmd_line.extend([self.match_arg, str(matchport)]) 189 | if self.other_args is not None: 190 | cmd_line.append(self.other_args) 191 | if realtime: 192 | cmd_line.extend([self.realtime_arg]) 193 | return cmd_line 194 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/portconfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import portpicker 4 | 5 | 6 | class Portconfig: 7 | """ 8 | A data class for ports used by participants to join a match. 9 | 10 | EVERY participant joining the match must send the same sets of ports to join successfully. 11 | SC2 needs 2 ports per connection (one for data, one as a 'header'), which is why the ports come in pairs. 12 | 13 | :param guests: number of non-hosting participants in a match (i.e. 1 less than the number of participants) 14 | :param server_ports: [int portA, int portB] 15 | :param player_ports: [[int port1A, int port1B], [int port2A, int port2B], ... ] 16 | 17 | .shared is deprecated, and should TODO be removed soon (once ladderbots' __init__.py doesnt specify them). 18 | 19 | .server contains the pair of ports used by the participant 'hosting' the match 20 | 21 | .players contains a pair of ports for every 'guest' (non-hosting participants) in the match 22 | E.g. for 1v1, there will be only 1 guest. For 2v2 (coming soonTM), there would be 3 guests. 23 | """ 24 | 25 | def __init__(self, guests=1, server_ports=None, player_ports=None): 26 | self.shared = None 27 | self._picked_ports = [] 28 | if server_ports: 29 | self.server = server_ports 30 | else: 31 | self.server = [portpicker.pick_unused_port() for _ in range(2)] 32 | self._picked_ports.extend(self.server) 33 | if player_ports: 34 | self.players = player_ports 35 | else: 36 | self.players = [[portpicker.pick_unused_port() for _ in range(2)] for _ in range(guests)] 37 | self._picked_ports.extend(port for player in self.players for port in player) 38 | 39 | def clean(self): 40 | while self._picked_ports: 41 | portpicker.return_port(self._picked_ports.pop()) 42 | 43 | def __str__(self): 44 | return f"Portconfig(shared={self.shared}, server={self.server}, players={self.players})" 45 | 46 | @property 47 | def as_json(self): 48 | return json.dumps({"shared": self.shared, "server": self.server, "players": self.players}) 49 | 50 | @classmethod 51 | def contiguous_ports(cls, guests=1, attempts=40): 52 | """Returns a Portconfig with adjacent ports""" 53 | for _ in range(attempts): 54 | start = portpicker.pick_unused_port() 55 | others = [start + j for j in range(1, 2 + guests * 2)] 56 | if all(portpicker.is_port_free(p) for p in others): 57 | server_ports = [start, others.pop(0)] 58 | player_ports = [] 59 | while others: 60 | player_ports.append([others.pop(0), others.pop(0)]) 61 | pc = cls(server_ports=server_ports, player_ports=player_ports) 62 | pc._picked_ports.append(start) 63 | return pc 64 | raise portpicker.NoFreePortFoundError() 65 | 66 | @classmethod 67 | def from_json(cls, json_data): 68 | data = json.loads(json_data) 69 | return cls(server_ports=data["server"], player_ports=data["players"]) 70 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/power_source.py: -------------------------------------------------------------------------------- 1 | from sc2.position import Point2 2 | 3 | 4 | class PowerSource: 5 | 6 | @classmethod 7 | def from_proto(cls, proto): 8 | return cls(Point2.from_proto(proto.pos), proto.radius, proto.tag) 9 | 10 | def __init__(self, position, radius, unit_tag): 11 | assert isinstance(position, Point2) 12 | assert radius > 0 13 | self.position = position 14 | self.radius = radius 15 | self.unit_tag = unit_tag 16 | 17 | def covers(self, position): 18 | return self.position.distance_to(position) <= self.radius 19 | 20 | def __repr__(self): 21 | return f"PowerSource({self.position}, {self.radius})" 22 | 23 | 24 | class PsionicMatrix: 25 | 26 | @classmethod 27 | def from_proto(cls, proto): 28 | return cls([PowerSource.from_proto(p) for p in proto]) 29 | 30 | def __init__(self, sources): 31 | self.sources = sources 32 | 33 | def covers(self, position): 34 | return any(source.covers(position) for source in self.sources) 35 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/protocol.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from contextlib import suppress 4 | 5 | from aiohttp import ClientWebSocketResponse 6 | from loguru import logger 7 | from s2clientprotocol import sc2api_pb2 as sc_pb 8 | 9 | from sc2.data import Status 10 | 11 | 12 | class ProtocolError(Exception): 13 | 14 | @property 15 | def is_game_over_error(self) -> bool: 16 | return self.args[0] in ["['Game has already ended']", "['Not supported if game has already ended']"] 17 | 18 | 19 | class ConnectionAlreadyClosed(ProtocolError): 20 | pass 21 | 22 | 23 | class Protocol: 24 | 25 | def __init__(self, ws): 26 | """ 27 | A class for communicating with an SCII application. 28 | :param ws: the websocket (type: aiohttp.ClientWebSocketResponse) used to communicate with a specific SCII app 29 | """ 30 | assert ws 31 | self._ws: ClientWebSocketResponse = ws 32 | self._status: Status = None 33 | 34 | async def __request(self, request): 35 | logger.debug(f"Sending request: {request !r}") 36 | try: 37 | await self._ws.send_bytes(request.SerializeToString()) 38 | except TypeError as exc: 39 | logger.exception("Cannot send: Connection already closed.") 40 | raise ConnectionAlreadyClosed("Connection already closed.") from exc 41 | logger.debug("Request sent") 42 | 43 | response = sc_pb.Response() 44 | try: 45 | response_bytes = await self._ws.receive_bytes() 46 | except TypeError as exc: 47 | if self._status == Status.ended: 48 | logger.info("Cannot receive: Game has already ended.") 49 | raise ConnectionAlreadyClosed("Game has already ended") from exc 50 | logger.error("Cannot receive: Connection already closed.") 51 | raise ConnectionAlreadyClosed("Connection already closed.") from exc 52 | except asyncio.CancelledError: 53 | # If request is sent, the response must be received before reraising cancel 54 | try: 55 | await self._ws.receive_bytes() 56 | except asyncio.CancelledError: 57 | logger.critical("Requests must not be cancelled multiple times") 58 | sys.exit(2) 59 | raise 60 | 61 | response.ParseFromString(response_bytes) 62 | logger.debug("Response received") 63 | return response 64 | 65 | async def _execute(self, **kwargs): 66 | assert len(kwargs) == 1, "Only one request allowed by the API" 67 | 68 | response = await self.__request(sc_pb.Request(**kwargs)) 69 | 70 | new_status = Status(response.status) 71 | if new_status != self._status: 72 | logger.info(f"Client status changed to {new_status} (was {self._status})") 73 | self._status = new_status 74 | 75 | if response.error: 76 | logger.debug(f"Response contained an error: {response.error}") 77 | raise ProtocolError(f"{response.error}") 78 | 79 | return response 80 | 81 | async def ping(self): 82 | result = await self._execute(ping=sc_pb.RequestPing()) 83 | return result 84 | 85 | async def quit(self): 86 | with suppress(ConnectionAlreadyClosed, ConnectionResetError): 87 | await self._execute(quit=sc_pb.RequestQuit()) 88 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/renderer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from s2clientprotocol import score_pb2 as score_pb 4 | 5 | from sc2.position import Point2 6 | 7 | 8 | class Renderer: 9 | 10 | def __init__(self, client, map_size, minimap_size): 11 | self._client = client 12 | 13 | self._window = None 14 | self._map_size = map_size 15 | self._map_image = None 16 | self._minimap_size = minimap_size 17 | self._minimap_image = None 18 | self._mouse_x, self._mouse_y = None, None 19 | self._text_supply = None 20 | self._text_vespene = None 21 | self._text_minerals = None 22 | self._text_score = None 23 | self._text_time = None 24 | 25 | async def render(self, observation): 26 | render_data = observation.observation.render_data 27 | 28 | map_size = render_data.map.size 29 | map_data = render_data.map.data 30 | minimap_size = render_data.minimap.size 31 | minimap_data = render_data.minimap.data 32 | 33 | map_width, map_height = map_size.x, map_size.y 34 | map_pitch = -map_width * 3 35 | 36 | minimap_width, minimap_height = minimap_size.x, minimap_size.y 37 | minimap_pitch = -minimap_width * 3 38 | 39 | if not self._window: 40 | # pylint: disable=C0415 41 | from pyglet.image import ImageData 42 | from pyglet.text import Label 43 | from pyglet.window import Window 44 | 45 | self._window = Window(width=map_width, height=map_height) 46 | self._window.on_mouse_press = self._on_mouse_press 47 | self._window.on_mouse_release = self._on_mouse_release 48 | self._window.on_mouse_drag = self._on_mouse_drag 49 | self._map_image = ImageData(map_width, map_height, "RGB", map_data, map_pitch) 50 | self._minimap_image = ImageData(minimap_width, minimap_height, "RGB", minimap_data, minimap_pitch) 51 | self._text_supply = Label( 52 | "", 53 | font_name="Arial", 54 | font_size=16, 55 | anchor_x="right", 56 | anchor_y="top", 57 | x=self._map_size[0] - 10, 58 | y=self._map_size[1] - 10, 59 | color=(200, 200, 200, 255), 60 | ) 61 | self._text_vespene = Label( 62 | "", 63 | font_name="Arial", 64 | font_size=16, 65 | anchor_x="right", 66 | anchor_y="top", 67 | x=self._map_size[0] - 130, 68 | y=self._map_size[1] - 10, 69 | color=(28, 160, 16, 255), 70 | ) 71 | self._text_minerals = Label( 72 | "", 73 | font_name="Arial", 74 | font_size=16, 75 | anchor_x="right", 76 | anchor_y="top", 77 | x=self._map_size[0] - 200, 78 | y=self._map_size[1] - 10, 79 | color=(68, 140, 255, 255), 80 | ) 81 | self._text_score = Label( 82 | "", 83 | font_name="Arial", 84 | font_size=16, 85 | anchor_x="left", 86 | anchor_y="top", 87 | x=10, 88 | y=self._map_size[1] - 10, 89 | color=(219, 30, 30, 255), 90 | ) 91 | self._text_time = Label( 92 | "", 93 | font_name="Arial", 94 | font_size=16, 95 | anchor_x="right", 96 | anchor_y="bottom", 97 | x=self._minimap_size[0] - 10, 98 | y=self._minimap_size[1] + 10, 99 | color=(255, 255, 255, 255), 100 | ) 101 | else: 102 | self._map_image.set_data("RGB", map_pitch, map_data) 103 | self._minimap_image.set_data("RGB", minimap_pitch, minimap_data) 104 | self._text_time.text = str(datetime.timedelta(seconds=(observation.observation.game_loop * 0.725) // 16)) 105 | if observation.observation.HasField("player_common"): 106 | self._text_supply.text = f"{observation.observation.player_common.food_used} / {observation.observation.player_common.food_cap}" 107 | self._text_vespene.text = str(observation.observation.player_common.vespene) 108 | self._text_minerals.text = str(observation.observation.player_common.minerals) 109 | if observation.observation.HasField("score"): 110 | # pylint: disable=W0212 111 | self._text_score.text = f"{score_pb._SCORE_SCORETYPE.values_by_number[observation.observation.score.score_type].name} score: {observation.observation.score.score}" 112 | 113 | await self._update_window() 114 | 115 | if self._client.in_game and (not observation.player_result) and self._mouse_x and self._mouse_y: 116 | await self._client.move_camera_spatial(Point2((self._mouse_x, self._minimap_size[0] - self._mouse_y))) 117 | self._mouse_x, self._mouse_y = None, None 118 | 119 | async def _update_window(self): 120 | self._window.switch_to() 121 | self._window.dispatch_events() 122 | 123 | self._window.clear() 124 | 125 | self._map_image.blit(0, 0) 126 | self._minimap_image.blit(0, 0) 127 | self._text_time.draw() 128 | self._text_score.draw() 129 | self._text_minerals.draw() 130 | self._text_vespene.draw() 131 | self._text_supply.draw() 132 | 133 | self._window.flip() 134 | 135 | def _on_mouse_press(self, x, y, button, _modifiers): 136 | if button != 1: # 1: mouse.LEFT 137 | return 138 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 139 | return 140 | self._mouse_x, self._mouse_y = x, y 141 | 142 | def _on_mouse_release(self, x, y, button, _modifiers): 143 | if button != 1: # 1: mouse.LEFT 144 | return 145 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 146 | return 147 | self._mouse_x, self._mouse_y = x, y 148 | 149 | def _on_mouse_drag(self, x, y, _dx, _dy, buttons, _modifiers): 150 | if not buttons & 1: # 1: mouse.LEFT 151 | return 152 | if x > self._minimap_size[0] or y > self._minimap_size[1]: 153 | return 154 | self._mouse_x, self._mouse_y = x, y 155 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/unit_command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Tuple, Union 4 | 5 | from sc2.constants import COMBINEABLE_ABILITIES 6 | from sc2.ids.ability_id import AbilityId 7 | from sc2.position import Point2 8 | 9 | if TYPE_CHECKING: 10 | from sc2.unit import Unit 11 | 12 | 13 | class UnitCommand: 14 | 15 | def __init__(self, ability: AbilityId, unit: Unit, target: Union[Unit, Point2] = None, queue: bool = False): 16 | """ 17 | :param ability: 18 | :param unit: 19 | :param target: 20 | :param queue: 21 | """ 22 | assert ability in AbilityId, f"ability {ability} is not in AbilityId" 23 | assert unit.__class__.__name__ == "Unit", f"unit {unit} is of type {type(unit)}" 24 | assert any( 25 | [ 26 | target is None, 27 | isinstance(target, Point2), 28 | unit.__class__.__name__ == "Unit", 29 | ] 30 | ), f"target {target} is of type {type(target)}" 31 | assert isinstance(queue, bool), f"queue flag {queue} is of type {type(queue)}" 32 | self.ability = ability 33 | self.unit = unit 34 | self.target = target 35 | self.queue = queue 36 | 37 | @property 38 | def combining_tuple(self) -> Tuple[AbilityId, Union[Unit, Point2], bool, bool]: 39 | return self.ability, self.target, self.queue, self.ability in COMBINEABLE_ABILITIES 40 | 41 | def __repr__(self): 42 | return f"UnitCommand({self.ability}, {self.unit}, {self.target}, {self.queue})" 43 | -------------------------------------------------------------------------------- /bots/loser_bot/sc2/wsl.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0911,W1510 2 | import os 3 | import re 4 | import subprocess 5 | from pathlib import Path, PureWindowsPath 6 | 7 | from loguru import logger 8 | 9 | ## This file is used for compatibility with WSL and shouldn't need to be 10 | ## accessed directly by any bot clients 11 | 12 | 13 | def win_path_to_wsl_path(path): 14 | """Convert a path like C:\\foo to /mnt/c/foo""" 15 | return Path("/mnt") / PureWindowsPath(re.sub("^([A-Z]):", lambda m: m.group(1).lower(), path)) 16 | 17 | 18 | def wsl_path_to_win_path(path): 19 | """Convert a path like /mnt/c/foo to C:\\foo""" 20 | return PureWindowsPath(re.sub("^/mnt/([a-z])", lambda m: m.group(1).upper() + ":", path)) 21 | 22 | 23 | def get_wsl_home(): 24 | """Get home directory of from Windows, even if run in WSL""" 25 | proc = subprocess.run(["powershell.exe", "-Command", "Write-Host -NoNewLine $HOME"], capture_output=True) 26 | 27 | if proc.returncode != 0: 28 | return None 29 | 30 | return win_path_to_wsl_path(proc.stdout.decode("utf-8")) 31 | 32 | 33 | RUN_SCRIPT = """$proc = Start-Process -NoNewWindow -PassThru "%s" "%s" 34 | if ($proc) { 35 | Write-Host $proc.id 36 | exit $proc.ExitCode 37 | } else { 38 | exit 1 39 | }""" 40 | 41 | 42 | def run(popen_args, sc2_cwd): 43 | """Run SC2 in Windows and get the pid so that it can be killed later.""" 44 | path = wsl_path_to_win_path(popen_args[0]) 45 | args = " ".join(popen_args[1:]) 46 | 47 | return subprocess.Popen( 48 | ["powershell.exe", "-Command", RUN_SCRIPT % (path, args)], 49 | cwd=sc2_cwd, 50 | stdout=subprocess.PIPE, 51 | universal_newlines=True, 52 | bufsize=1, 53 | ) 54 | 55 | 56 | def kill(wsl_process): 57 | """Needed to kill a process started with WSL. Returns true if killed successfully.""" 58 | # HACK: subprocess and WSL1 appear to have a nasty interaction where 59 | # any streams are never closed and the process is never considered killed, 60 | # despite having an exit code (this works on WSL2 as well, but isn't 61 | # necessary). As a result, 62 | # 1: We need to read using readline (to make sure we block long enough to 63 | # get the exit code in the rare case where the user immediately hits ^C) 64 | out = wsl_process.stdout.readline().rstrip() 65 | # 2: We need to use __exit__, since kill() calls send_signal(), which thinks 66 | # the process has already exited! 67 | wsl_process.__exit__(None, None, None) 68 | proc = subprocess.run(["taskkill.exe", "-f", "-pid", out], capture_output=True) 69 | return proc.returncode == 0 # Returns 128 on failure 70 | 71 | 72 | def detect(): 73 | """Detect the current running version of WSL, and bail out if it doesn't exist""" 74 | # Allow disabling WSL detection with an environment variable 75 | if os.getenv("SC2_WSL_DETECT", "1") == "0": 76 | return None 77 | 78 | wsl_name = os.environ.get("WSL_DISTRO_NAME") 79 | if not wsl_name: 80 | return None 81 | 82 | try: 83 | wsl_proc = subprocess.run(["wsl.exe", "--list", "--running", "--verbose"], capture_output=True) 84 | except (OSError, ValueError): 85 | return None 86 | if wsl_proc.returncode != 0: 87 | return None 88 | 89 | # WSL.exe returns a bunch of null characters for some reason, as well as 90 | # windows-style linebreaks. It's inconsistent about how many \rs it uses 91 | # and this could change in the future, so strip out all junk and split by 92 | # Unix-style newlines for safety's sake. 93 | lines = re.sub(r"\000|\r", "", wsl_proc.stdout.decode("utf-8")).split("\n") 94 | 95 | def line_has_proc(ln): 96 | return re.search("^\\s*[*]?\\s+" + wsl_name, ln) 97 | 98 | def line_version(ln): 99 | return re.sub("^.*\\s+(\\d+)\\s*$", "\\1", ln) 100 | 101 | versions = [line_version(ln) for ln in lines if line_has_proc(ln)] 102 | 103 | try: 104 | version = versions[0] 105 | if int(version) not in [1, 2]: 106 | return None 107 | except (ValueError, IndexError): 108 | return None 109 | 110 | logger.info(f"WSL version {version} detected") 111 | 112 | if version == "2" and not (os.environ.get("SC2CLIENTHOST") and os.environ.get("SC2SERVERHOST")): 113 | logger.warning("You appear to be running WSL2 without your hosts configured correctly.") 114 | logger.warning("This may result in SC2 staying on a black screen and not connecting to your bot.") 115 | logger.warning("Please see the python-sc2 README for WSL2 configuration instructions.") 116 | 117 | return "WSL" + version 118 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | ARENA_CLIENT_ID = "aiarenaclient_local" # ID of arenaclient. Used for AiArena 2 | ROUNDS_PER_RUN = -1 # Set to -1 to ignore this 3 | BASE_WEBSITE_URL = "" 4 | DEBUG_MODE = true # Enables debug mode for more logging 5 | PYTHON = "python" # Which python version to use 6 | RUN_TYPE = "local" # Run on AiArena, locally or test (options: "test", "local", "aiarena") 7 | # Secure mode will ignore the BOTS_DIRECTORY configuration setting and instead run each bot in their home directory. 8 | SECURE_MODE = false 9 | 10 | # LOGGING 11 | LOGGING_LEVEL = "debug" #info,debug,error,trace 12 | LOG_ROOT = "/logs" 13 | 14 | # Directories 15 | REPLAYS_DIRECTORY = "/replays" 16 | BOTS_DIRECTORY = "/bots" # Ignored when SECURE_MODE == True 17 | 18 | # STARCRAFT 19 | MAX_GAME_TIME = 80640 20 | MAX_REAL_TIME = 7200 # 2 hours in seconds 21 | MAX_FRAME_TIME = 40 # milliseconds 22 | STRIKES = 10 23 | REALTIME = false 24 | VISUALIZE = false 25 | 26 | # MATCHES 27 | DISABLE_DEBUG = true 28 | VALIDATE_RACE = false 29 | 30 | 31 | # Local 32 | MATCHES_FILE="matches" 33 | RESULTS_FILE="results.json" 34 | 35 | # Controllers 36 | BOT_CONT_1_HOST="127.0.0.1" 37 | BOT_CONT_1_PORT=8081 38 | BOT_CONT_2_HOST="127.0.0.1" 39 | BOT_CONT_2_PORT=8082 40 | SC2_CONT_HOST="127.0.0.1" 41 | SC2_CONT_PORT=8083 -------------------------------------------------------------------------------- /docker-compose-host-network.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sc2_controller: 3 | network_mode: host 4 | image: aiarena/arenaclient-sc2:v0.6.10 5 | environment: 6 | - "ACSC2_PORT=8083" 7 | - "ACSC2_PROXY_HOST=127.0.0.1" 8 | volumes: 9 | - "./logs:/logs" # a sc2_controller folder will be created in the logs folder 10 | # SC2 Maps Path 11 | # Set this as "- PATH_TO_YOUR_MAPS_FOLDER:/root/StarCraftII/maps" 12 | # - C:\Program Files (x86)\StarCraft II\Maps:/root/StarCraftII/maps # Standard windows SC2 maps path 13 | - ./maps:/root/StarCraftII/maps # Local maps folder 14 | # - ~/StarCraftII/maps:/root/StarCraftII/maps # Relatively standard linux SC2 maps path 15 | 16 | bot_controller1: 17 | network_mode: host 18 | image: aiarena/arenaclient-bot:v0.6.10 19 | volumes: 20 | - "./bots:/bots" 21 | - "./logs/bot_controller1:/logs" 22 | environment: 23 | - "ACBOT_PORT=8081" 24 | - "ACBOT_PROXY_HOST=127.0.0.1" 25 | 26 | bot_controller2: 27 | network_mode: host 28 | image: aiarena/arenaclient-bot:v0.6.10 29 | volumes: 30 | - "./bots:/bots" 31 | - "./logs/bot_controller2:/logs" 32 | environment: 33 | - "ACBOT_PORT=8082" 34 | - "ACBOT_PROXY_HOST=127.0.0.1" 35 | 36 | proxy_controller: 37 | network_mode: host 38 | image: aiarena/arenaclient-proxy:v0.6.10 39 | environment: 40 | - "ACPROXY_PORT=8080" 41 | - "ACPROXY_BOT_CONT_1_HOST=127.0.0.1" 42 | - "ACPROXY_BOT_CONT_2_HOST=127.0.0.1" 43 | - "ACPROXY_SC2_CONT_HOST=127.0.0.1" 44 | volumes: 45 | - "./matches:/app/matches" 46 | - "./config.toml:/app/config.toml" 47 | - "./results.json:/app/results.json" 48 | - "./replays:/replays" 49 | - "./logs:/logs" # a proxy_controller folder will be created in the logs folder 50 | -------------------------------------------------------------------------------- /docker-compose-multithread-example.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sc2_controller: 3 | image: aiarena/arenaclient-sc2:v0.6.10 4 | environment: 5 | - "ACSC2_PORT=8083" 6 | - "ACSC2_PROXY_HOST=proxy_controller" 7 | volumes: 8 | - "./runners/${COMPOSE_PROJECT_NAME}/logs:/logs" # a sc2_controller folder will be created in the logs folder 9 | # SC2 Maps Path 10 | # Set this as "- PATH_TO_YOUR_MAPS_FOLDER:/root/StarCraftII/maps" 11 | # - C:\Program Files (x86)\StarCraft II\Maps:/root/StarCraftII/maps # Standard windows SC2 maps path 12 | - ./maps:/root/StarCraftII/maps # Local maps folder 13 | # - ~/StarCraftII/maps:/root/StarCraftII/maps # Relatively standard linux SC2 maps path 14 | 15 | bot_controller1: 16 | image: aiarena/arenaclient-bot:v0.6.10 17 | volumes: 18 | - "./bots:/bots" 19 | - "./runners/${COMPOSE_PROJECT_NAME}/logs/bot_controller1:/logs" 20 | environment: 21 | - "ACBOT_PORT=8081" 22 | - "ACBOT_PROXY_HOST=proxy_controller" 23 | 24 | bot_controller2: 25 | image: aiarena/arenaclient-bot:v0.6.10 26 | volumes: 27 | - "./bots:/bots" 28 | - "./runners/${COMPOSE_PROJECT_NAME}/logs/bot_controller2:/logs" 29 | environment: 30 | - "ACBOT_PORT=8082" 31 | - "ACBOT_PROXY_HOST=proxy_controller" 32 | 33 | proxy_controller: 34 | image: aiarena/arenaclient-proxy:v0.6.10 35 | environment: 36 | - "ACPROXY_PORT=8080" 37 | - "ACPROXY_BOT_CONT_1_HOST=bot_controller1" 38 | - "ACPROXY_BOT_CONT_2_HOST=bot_controller2" 39 | - "ACPROXY_SC2_CONT_HOST=sc2_controller" 40 | volumes: 41 | - "./runners/${COMPOSE_PROJECT_NAME}/matches:/app/matches" 42 | - "./config.toml:/app/config.toml" 43 | - "./results.json:/app/results.json" 44 | - "./runners/${COMPOSE_PROJECT_NAME}/replays:/replays" 45 | - "./runners/${COMPOSE_PROJECT_NAME}/logs:/logs" # a proxy_controller folder will be created in the logs folder 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sc2_controller: 3 | image: aiarena/arenaclient-sc2:v0.6.10 4 | environment: 5 | - "ACSC2_PORT=8083" 6 | - "ACSC2_PROXY_HOST=proxy_controller" 7 | volumes: 8 | - "./logs/sc2_controller:/logs" # a sc2_controller folder will be created in the logs folder 9 | # SC2 Maps Path 10 | # Set this as "- PATH_TO_YOUR_MAPS_FOLDER:/root/StarCraftII/maps" 11 | # - C:\Program Files (x86)\StarCraft II\Maps:/root/StarCraftII/maps # Standard windows SC2 maps path 12 | - ./maps:/root/StarCraftII/maps # Local maps folder 13 | # - ~/StarCraftII/maps:/root/StarCraftII/maps # Relatively standard linux SC2 maps path 14 | 15 | bot_controller1: 16 | image: aiarena/arenaclient-bot:v0.6.10 17 | volumes: 18 | - "./bots:/bots" 19 | - "./logs/bot_controller1:/logs" 20 | environment: 21 | - "ACBOT_PORT=8081" 22 | - "ACBOT_PROXY_HOST=proxy_controller" 23 | 24 | bot_controller2: 25 | image: aiarena/arenaclient-bot:v0.6.10 26 | volumes: 27 | - "./bots:/bots" 28 | - "./logs/bot_controller2:/logs" 29 | environment: 30 | - "ACBOT_PORT=8082" 31 | - "ACBOT_PROXY_HOST=proxy_controller" 32 | 33 | proxy_controller: 34 | image: aiarena/arenaclient-proxy:v0.6.10 35 | environment: 36 | - "ACPROXY_PORT=8080" 37 | - "ACPROXY_BOT_CONT_1_HOST=bot_controller1" 38 | - "ACPROXY_BOT_CONT_2_HOST=bot_controller2" 39 | - "ACPROXY_SC2_CONT_HOST=sc2_controller" 40 | volumes: 41 | - "./matches:/app/matches" 42 | - "./config.toml:/app/config.toml" 43 | - "./results.json:/app/results.json" 44 | - "./replays:/replays" 45 | - "./logs:/logs" # a proxy_controller folder will be created in the logs folder -------------------------------------------------------------------------------- /img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiarena/local-play-bootstrap/d4ac456bc63c6eb926dfee17c1b2e2cd128851e1/img/download.png -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /maps/AcropolisAIE.SC2Map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiarena/local-play-bootstrap/d4ac456bc63c6eb926dfee17c1b2e2cd128851e1/maps/AcropolisAIE.SC2Map -------------------------------------------------------------------------------- /matches: -------------------------------------------------------------------------------- 1 | # Bot1 ID, Bot1 name, Bot1 race, Bot1 type, Bot2 ID, Bot2 name, Bot2 race, Bot2 type, Map 2 | 1,basic_bot,T,python,2,loser_bot,T,python,AcropolisAIE 3 | -------------------------------------------------------------------------------- /multithread-example.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | import subprocess 5 | from multiprocessing.dummy import Pool as ThreadPool 6 | 7 | from loguru import logger 8 | 9 | ################### 10 | # RUNNER SETTINGS # 11 | ################### 12 | 13 | # all the runner files will be inside this path 14 | root_runners_path = f"./runners/" 15 | 16 | # if True, delete all the runner files before starting 17 | clean_run = True 18 | 19 | # In case you want to use a different docker-compose file 20 | docker_compose_file = "./docker-compose-multithread-example.yml" 21 | 22 | # run this many matches at a time 23 | num_runners = 3 24 | 25 | ######################### 26 | # MATCH GENERATION CODE # 27 | ######################### 28 | 29 | # This is an example of how to generate matches 30 | 31 | bot = ["basic_bot", "T", "python"] 32 | opponents = [ 33 | ["loser_bot", "T", "python"], 34 | ["loser_bot", "T", "python"], 35 | ["loser_bot", "T", "python"], 36 | ] 37 | map_list = ["BerlingradAIE"] 38 | num_games = len(opponents) 39 | 40 | 41 | def get_matches_to_play(): 42 | """ 43 | Returns a list of matches to play 44 | Edit this function to generate matches your preferred way. 45 | """ 46 | matches = [] 47 | for x in range(num_games): 48 | # we add 1 to x because we want the runner id to start at 1 49 | runner_id = x + 1 50 | map = random.choice(map_list) 51 | opponent = opponents[x % len(opponents)] 52 | matches.append((runner_id, bot, opponent, map)) 53 | return matches 54 | 55 | 56 | ######################################################## 57 | # Hopefully you shouldn't need to edit below this line # 58 | ######################################################## 59 | 60 | def play_game(match): 61 | try: 62 | # prepare the match runner 63 | runner_id = match[0] 64 | logger.info(f"[{runner_id}] {match[1][0]}vs{match[2][0]} on {match[3]} starting") 65 | runner_dir = prepare_runner_dir(runner_id) 66 | prepare_matches_and_results_files(match, runner_dir) 67 | 68 | # start the match running 69 | command = f'docker compose -p {runner_id} -f {docker_compose_file} up' 70 | subprocess.Popen( 71 | command, 72 | shell=True, 73 | ).communicate() 74 | 75 | except Exception as error: 76 | logger.error("[ERROR] {0}".format(str(error))) 77 | 78 | 79 | def prepare_matches_and_results_files(match, runner_dir): 80 | file = open(f"{runner_dir}/matches", "w") 81 | # match[1][0] and match[2][1] are twice, because we re-use the bot name as the bot id 82 | file.write( 83 | f"{match[1][0]},{match[1][0]},{match[1][1]},{match[1][2]}," # bot1 84 | f"{match[2][0]},{match[2][0]},{match[2][1]},{match[2][2]}," # bot2 85 | f"{match[3]}") # map 86 | file.close() 87 | 88 | # touch results.json 89 | file = open(f"{runner_dir}/results.json", "w") 90 | file.close() 91 | 92 | 93 | def handleRemoveReadonly(func, path, exc): 94 | import stat 95 | if not os.access(path, os.W_OK): 96 | # Is the error an access error ? 97 | os.chmod(path, stat.S_IWUSR) 98 | func(path) 99 | else: 100 | raise 101 | 102 | 103 | def prepare_runner_dir(dir_name) -> str: 104 | runner_dir = f"{root_runners_path}/{dir_name}" 105 | if not os.path.exists(runner_dir): 106 | os.makedirs(runner_dir) 107 | return runner_dir 108 | 109 | 110 | def prepare_root_dir(): 111 | if os.path.exists(root_runners_path) and clean_run: 112 | shutil.rmtree(root_runners_path, onerror=handleRemoveReadonly) 113 | if not os.path.exists(root_runners_path): 114 | os.makedirs(root_runners_path) 115 | 116 | 117 | def main(): 118 | pool = ThreadPool(num_runners) 119 | 120 | prepare_root_dir() 121 | 122 | matches = get_matches_to_play() 123 | 124 | pool.map(play_game, matches) 125 | pool.close() 126 | pool.join() 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /replays/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /results.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | ] 4 | } --------------------------------------------------------------------------------