def __init__( self, raw_game_data: Any, raw_game_info: Any, raw_observation: Any ) -> None: """ Set up variables for use within Creeper. Args: raw_game_data (Any): self.game_data from main instance raw_game_info (Any): self.game_info from main instance raw_observation (Any): self.game_state from main instance Returns: None """ self.bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) self.bot._initialize_variables() self.bot._prepare_start( client=None, player_id=1, game_info=game_info, game_data=game_data ) self.bot._prepare_step(state=game_state, proto_game_info=raw_game_info) self.pathing = PathManager(raw_game_data, raw_game_info, raw_observation) self.pf = PathFind(self.pathing.map_grid)
def __init__( self, raw_game_data: Any, raw_game_info: Any, raw_observation: Any ) -> None: """ Set up variables for use within PathMangager. Args: raw_game_data (Any): self.game_data from main instance raw_game_info (Any): self.game_info from main instance raw_observation (Any): self.game_state from main instance Returns: None """ self.bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) self.bot._initialize_variables() self.bot._prepare_start( client=None, player_id=1, game_info=game_info, game_data=game_data ) self.bot._prepare_step(state=game_state, proto_game_info=raw_game_info) map_name = game_info.map_name self.pathing_dict: Dict[int, PathDict] = {} map_width = game_info.map_size.width map_height = game_info.map_size.height raw_grid = np.zeros((map_width, map_height)) for x in range(map_width): for y in range(map_height): pos = Point2((x, y)) raw_grid[x][y] = game_info.pathing_grid[pos] self.map_grid = np.rot90(raw_grid.astype(int)) np.save(f"map_grids/{map_name}_grid", self.map_grid) self.pf = PathFind(self.map_grid)
def test_bot_ai(self, bot: BotAI): bot._game_info.map_ramps, bot._game_info.vision_blockers = bot._game_info._find_ramps_and_vision_blockers( ) assert bot.main_base_ramp # Test if any ramp was found # Clear cache for expansion locations, recalculate and time it if hasattr(bot, "_cache_expansion_locations"): delattr(bot, "_cache_expansion_locations") t0 = time.perf_counter() bot._find_expansion_locations() t1 = time.perf_counter() print(f"Time to calculate expansion locations: {t1-t0} s") # TODO: Cache all expansion positions for a map and check if it is the same # BelShirVestigeLE has only 10 bases - perhaps it should be removed since it was a WOL / HOTS map assert len( bot.expansion_locations_list ) >= 10, f"Too few expansions found: {len(bot.expansion_locations_list)}" # Honorgrounds LE has 24 bases assert ( len(bot.expansion_locations_list) <= 24 ), f"Too many expansions found: {len(bot.expansion_locations_list)}" # On N player maps, it is expected that there are N*X bases because of symmetry, at least for maps designed for 1vs1 assert (len(bot.expansion_locations_list) % (len(bot.enemy_start_locations) + 1) == 0), f"{bot.expansion_locations_list}" # Test if bot start location is in expansion locations assert bot.townhalls.random.position in set( bot.expansion_locations_list ), f'This error might occur if you are running the tests locally using command "pytest test/", possibly because you are using an outdated cache.py version, but it should not occur when using docker and pipenv.\n{bot.townhalls.random.position}, {bot.expansion_locations_list}' # Test if enemy start locations are in expansion locations for location in bot.enemy_start_locations: assert location in set( bot.expansion_locations_list ), f"{location}, {bot.expansion_locations_list}" # Each expansion is supposed to have at least one geysir and 6-12 minerals for expansion, resource_positions in bot.expansion_locations_dict.items( ): assert isinstance(expansion, Point2) assert isinstance(resource_positions, Units) if resource_positions: assert isinstance(resource_positions[0], Unit) # Neon violet has bases with just 6 resources. I think that was the back corner base with 4 minerals and 2 vespene # Odyssey has bases with 10 mineral patches and 2 geysirs # Blood boil returns 21? assert ( 6 <= len(resource_positions) <= 12 ), f"{len(resource_positions)} resource fields in one base on map {bot.game_info.map_name}" assert bot.owned_expansions == { bot.townhalls.first.position: bot.townhalls.first }
def _abilities_all_units(bot_object : BotAI) -> Counter: """ Cache for the already_pending function """ abilities_amount = Counter() for unit in bot_object.units({UnitTypeId.SCV, UnitTypeId.DRONE, UnitTypeId.PROBE}): for order in unit.orders: abilities_amount[order.ability] += 1 return abilities_amount
def get_map_specific_bot(map_path: Path) -> BotAI: assert map_path in MAPS with lzma.open(str(map_path.absolute()), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) # Build fresh bot object, and load the pickle'd data into the bot object bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) bot._initialize_variables() bot._prepare_start(client=None, player_id=1, game_info=game_info, game_data=game_data) bot._prepare_step(state=game_state, proto_game_info=raw_game_info) return bot
def get_map_specific_bots() -> Iterable[BotAI]: folder = os.path.dirname(__file__) subfolder_name = "pickle_data" pickle_folder_path = os.path.join(folder, subfolder_name) files = os.listdir(pickle_folder_path) for file in (f for f in files if f.endswith(".xz")): with lzma.open(os.path.join(folder, subfolder_name, file), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) # Build fresh bot object, and load the pickle'd data into the bot object bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) bot._initialize_variables() bot._prepare_start(client=None, player_id=1, game_info=game_info, game_data=game_data) bot._prepare_step(state=game_state, proto_game_info=raw_game_info) yield bot
class Creeper: """Spread creep.""" def __init__( self, raw_game_data: Any, raw_game_info: Any, raw_observation: Any ) -> None: """ Set up variables for use within Creeper. Args: raw_game_data (Any): self.game_data from main instance raw_game_info (Any): self.game_info from main instance raw_observation (Any): self.game_state from main instance Returns: None """ self.bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) self.bot._initialize_variables() self.bot._prepare_start( client=None, player_id=1, game_info=game_info, game_data=game_data ) self.bot._prepare_step(state=game_state, proto_game_info=raw_game_info) self.pathing = PathManager(raw_game_data, raw_game_info, raw_observation) self.pf = PathFind(self.pathing.map_grid) def check_tumor_position( self, possible_position: Point2, combined_grid: Any ) -> int: """ Calculate the number of tiles the tumor position would cover. Arg: possible_position (Point2): the point being checked. combined_grid (Any): ndarray that's the pathfinding lib's path map and the creep map added together Returns: int: the number of tiles that would be covered. """ x, y = floor(possible_position[0]), floor(possible_position[1]) future_tiles = 0 for i in range(-10, 11): for j in range(-10, 11): if 8 <= abs(j) <= 10: if abs(i) <= 6 - 2 * (abs(j) - 8): future_tiles += combined_grid[Point2((x + i, y + j))] % 2 elif abs(j) == 7: if abs(i) <= 7: future_tiles += combined_grid[Point2((x + i, y + j))] % 2 elif 3 <= abs(j) <= 6: if abs(i) <= 9 - floor(abs(j) / 5): future_tiles += combined_grid[Point2((x + i, y + j))] % 2 else: if combined_grid[Point2((x + i, y + j))] == 1: future_tiles += 1 return future_tiles async def find_position( self, tumor: Unit, tumor_positions: Set[Point2], creep_grid: Any, pathable_map: Any, ) -> Point2: """ Find the location to spread the tumor to. Args: tumor (Unit): the creep tumor ready to be spread tumor_positions (Set[Point2]): list of existing tumor locations creep_grid (ndarray): the creep grid from the main bot pathable_map (ndarray): the pathfinding lib's pathable map Returns: Point2: where to spread the tumor. """ path_creep_map = creep_grid + pathable_map tposx, tposy = tumor.position.x, tumor.position.y max_tiles = 0 location = None for i in range(-10, 11): for j in range(-10, 11): if 81 <= i ** 2 + j ** 2 <= 105: pos = Point2((tposx + i, tposy + j)) if pos not in tumor_positions: if path_creep_map[floor(pos[0])][floor(pos[1])] != 2: continue tiles = self.check_tumor_position((pos), path_creep_map) if tiles > max_tiles: max_tiles = tiles location = pos if max_tiles < 75: floored_unit_pos = Point2((floor(tposx), floor(tposy))) floored_e_base = Point2( ( floor(self.bot.enemy_start_locations[0].position.x), floor(self.bot.enemy_start_locations[0].position.y), ) ) path_to_e_base = self.pf.find_path(floored_unit_pos, floored_e_base)[0] if path_to_e_base: for k in range(9, 5, -1): pos = path_to_e_base[k] if path_creep_map[pos[0]][pos[1]] == 2: location = Point2(pos) break if location: return location else: return tumor.position
async def _play_game_ai( client: Client, player_id: int, ai: BotAI, realtime: bool, game_time_limit: Optional[int] ) -> Result: gs: GameState = None async def initialize_first_step() -> Optional[Result]: nonlocal gs ai._initialize_variables() game_data = await client.get_game_data() game_info = await client.get_game_info() ping_response = await client.ping() # This game_data will become self.game_data in botAI ai._prepare_start( client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build ) state = await client.observation() # check game result every time we get the observation if client._game_result: await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] gs = GameState(state.observation) proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) try: ai._prepare_step(gs, proto_game_info) await ai.on_before_start() ai._prepare_first_step() await ai.on_start() # TODO Catching too general exception Exception (broad-except) # pylint: disable=W0703 except Exception: logger.exception("AI on_start threw an error") logger.error("Resigning due to previous error") await ai.on_end(Result.Defeat) return Result.Defeat result = await initialize_first_step() if result is not None: return result async def run_bot_iteration(iteration: int): nonlocal gs logger.debug(f"Running AI step, it={iteration} {gs.game_loop / 22.4:.2f}s") # Issue event like unit created or unit destroyed await ai.issue_events() await ai.on_step(iteration) await ai._after_step() logger.debug("Running AI step: done") # Only used in realtime=True previous_state_observation = None for iteration in range(10**10): if realtime and gs: # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game'] with suppress(ProtocolError): requested_step = gs.game_loop + client.game_step state = await client.observation(requested_step) # If the bot took too long in the previous observation, request another observation one frame after if state.observation.observation.game_loop > requested_step: logger.debug("Skipped a step in realtime=True") previous_state_observation = state.observation state = await client.observation(state.observation.observation.game_loop + 1) else: state = await client.observation() # check game result every time we get the observation if client._game_result: await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] gs = GameState(state.observation, previous_state_observation) previous_state_observation = None logger.debug(f"Score: {gs.score.score}") if game_time_limit and gs.game_loop / 22.4 > game_time_limit: await ai.on_end(Result.Tie) return Result.Tie proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo()) ai._prepare_step(gs, proto_game_info) await run_bot_iteration(iteration) # Main bot loop if not realtime: if not client.in_game: # Client left (resigned) the game await ai.on_end(client._game_result[player_id]) return client._game_result[player_id] # TODO: In bot vs bot, if the other bot ends the game, this bot gets stuck in requesting an observation when using main.py:run_multiple_games await client.step() return Result.Undecided
def import_bot_instance( raw_game_data: Response, raw_game_info: Response, raw_observation: ResponseObservation, ) -> BotAI: """ import_bot_instance DocString """ bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) # noinspection PyProtectedMember bot._initialize_variables() # noinspection PyProtectedMember bot._prepare_start(client=None, player_id=1, game_info=game_info, game_data=game_data) # noinspection PyProtectedMember bot._prepare_first_step() # noinspection PyProtectedMember bot._prepare_step(state=game_state, proto_game_info=raw_game_info) # noinspection PyProtectedMember bot._find_expansion_locations() return bot
def test_bot_ai(self, bot: BotAI): # Test initial bot attributes at game start # Properties from _prepare_start assert 1 <= bot.player_id <= 2 assert isinstance(bot.race, Race) assert isinstance(bot.enemy_race, Race) # Properties from _prepare_step assert bot.units.amount == bot.townhalls.amount + bot.workers.amount assert bot.workers.amount == 12 assert bot.townhalls.amount == 1 assert bot.geysers.amount == 0 assert bot.minerals == 50 assert bot.vespene == 0 assert bot.supply_army == 0 assert bot.supply_workers == 12 assert bot.supply_cap == 15 assert bot.supply_used == 12 assert bot.supply_left == 3 assert bot.idle_worker_count == 0 assert bot.army_count == 0 # Test bot_ai functions assert bot.time == 0 assert bot.time_formatted in {"0:00", "00:00"} assert bot.start_location is None # Is populated by main.py bot._game_info.player_start_location = bot.townhalls.random.position assert bot.townhalls.random.position not in bot.enemy_start_locations assert bot.known_enemy_units == Units([]) assert bot.known_enemy_structures == Units([]) bot._game_info.map_ramps, bot._game_info.vision_blockers = bot._game_info._find_ramps_and_vision_blockers() assert bot.main_base_ramp # Test if any ramp was found # TODO: Cache all expansion positions for a map and check if it is the same assert len(bot.expansion_locations) >= 10 # On N player maps, it is expected that there are N*X bases because of symmetry, at least for 1vs1 maps assert ( len(bot.expansion_locations) % (len(bot.enemy_start_locations) + 1) == 0 ), f"{set(bot.expansion_locations.keys())}" # Test if bot start location is in expansion locations assert bot.townhalls.random.position in set( bot.expansion_locations.keys() ), f'This error might occur if you are running the tests locally using command "pytest test/", possibly because you are using an outdated cache.py version, but it should not occur when using docker and pipenv.\n{bot.townhalls.random.position}, {set(bot.expansion_locations.keys())}' # Test if enemy start locations are in expansion locations for location in bot.enemy_start_locations: assert location in set(bot.expansion_locations.keys()), f"{location}, {bot.expansion_locations.keys()}" # The following functions need to be tested by autotest_bot.py because they use API query which isn't available here as this file only uses the pickle files and is not able to interact with the API as SC2 is not running while this test runs # get_available_abilities # expand_now # get_next_expansion # distribute_workers assert bot.owned_expansions == {bot.townhalls.first.position: bot.townhalls.first} assert bot.can_feed(UnitTypeId.MARINE) assert bot.can_feed(UnitTypeId.SIEGETANK) assert not bot.can_feed(UnitTypeId.THOR) assert not bot.can_feed(UnitTypeId.BATTLECRUISER) assert not bot.can_feed(UnitTypeId.IMMORTAL) assert bot.can_afford(UnitTypeId.SCV) assert bot.can_afford(UnitTypeId.MARINE) assert not bot.can_afford(UnitTypeId.SIEGETANK) assert not bot.can_afford(UnitTypeId.BATTLECRUISER) assert not bot.can_afford(UnitTypeId.MARAUDER) # can_cast worker = bot.workers.random assert bot.select_build_worker(worker.position) == worker for w in bot.workers: if w == worker: continue assert bot.select_build_worker(w.position) != worker # can_place # find_placement assert bot.already_pending_upgrade(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UnitTypeId.SCV) == 0 # build # do # do_actions # chat_send assert 0 < bot.get_terrain_height(worker) assert bot.in_placement_grid(worker) assert bot.in_pathing_grid(worker) assert bot.is_visible(worker) # The pickle data was created by a terran bot, so there is no creep under any worker assert not bot.has_creep(worker)
class PathManager: """Manage unit pathing.""" def __init__( self, raw_game_data: Any, raw_game_info: Any, raw_observation: Any ) -> None: """ Set up variables for use within PathMangager. Args: raw_game_data (Any): self.game_data from main instance raw_game_info (Any): self.game_info from main instance raw_observation (Any): self.game_state from main instance Returns: None """ self.bot = BotAI() game_data = GameData(raw_game_data.data) game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) self.bot._initialize_variables() self.bot._prepare_start( client=None, player_id=1, game_info=game_info, game_data=game_data ) self.bot._prepare_step(state=game_state, proto_game_info=raw_game_info) map_name = game_info.map_name self.pathing_dict: Dict[int, PathDict] = {} map_width = game_info.map_size.width map_height = game_info.map_size.height raw_grid = np.zeros((map_width, map_height)) for x in range(map_width): for y in range(map_height): pos = Point2((x, y)) raw_grid[x][y] = game_info.pathing_grid[pos] self.map_grid = np.rot90(raw_grid.astype(int)) np.save(f"map_grids/{map_name}_grid", self.map_grid) self.pf = PathFind(self.map_grid) def add_to_path_dict(self, unit: Unit, destination: Point2) -> None: """ Add unit's path to the path storage dictionary. Args: unit (Unit): the unit for pathing destination (Point2): where the unit is going Returns: None """ self.pathing_dict[unit.tag] = {"path": [], "step": 0} raw_unit_pos = unit.position floored_unit_pos = Point2((floor(raw_unit_pos.x), floor(raw_unit_pos.y))) floored_dest = Point2((floor(destination[0]), floor(destination[1]))) self.pathing_dict[unit.tag]["path"] = self.pf.find_path( floored_unit_pos, floored_dest )[0] def follow_path(self, unit: Unit, default: Point2) -> Point2: """ Follow the path set or set a new one if none exists. Args: unit (Unit): the unit moving Returns: Point2: the location to attack """ if ( len( self.bot.structures.filter( lambda unit: unit.type_id in {UnitTypeId.NYDUSNETWORK, UnitTypeId.NYDUSCANAL} ) ) < 2 ): if unit.tag not in self.pathing_dict: self.add_to_path_dict(unit, tuple(default)) advance_factor = int(unit.movement_speed) + 2 self.pathing_dict[unit.tag]["step"] += advance_factor curr_step = self.pathing_dict[unit.tag]["step"] if curr_step >= len(self.pathing_dict[unit.tag]["path"]): curr_step = len(self.pathing_dict[unit.tag]["path"]) - 1 if curr_step < 0: return default a_move_to = Point2(self.pathing_dict[unit.tag]["path"][curr_step]) if curr_step == len(self.pathing_dict[unit.tag]["path"]) - 1: del self.pathing_dict[unit.tag] return a_move_to