From d4224bc9fe77d52a3d4fb3ae4758c4ad4b57625d Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 21 Dec 2024 16:45:24 +0000 Subject: [PATCH 1/5] feat: support protoss natural wall user configuration --- src/ares/building_placements.yml | 85 +++ src/ares/config_parser.py | 40 +- src/ares/consts.py | 15 + src/ares/main.py | 7 +- src/ares/managers/manager_mediator.py | 37 +- src/ares/managers/placement_manager.py | 703 +++++++++++++++---------- 6 files changed, 591 insertions(+), 296 deletions(-) create mode 100644 src/ares/building_placements.yml diff --git a/src/ares/building_placements.yml b/src/ares/building_placements.yml new file mode 100644 index 00000000..111e4cee --- /dev/null +++ b/src/ares/building_placements.yml @@ -0,0 +1,85 @@ +Protoss: + AbyssalReef: + VsZergNatWall: + UpperSpawn: + FirstPylon: [[64., 105.]] + Pylons: [[63., 112.]] + ThreeByThrees: [[68.5, 109.5], [66.5, 106.5], [60.5, 106.5]] + StaticDefences: [[64., 110.]] + GateKeeper: [[62.25, 105.86]] + LowerSpawn: + FirstPylon: [[136., 39.]] + Pylons: [[137., 32.]] + ThreeByThrees: [[131.5, 34.5], [133.5, 37.5], [139.5, 37.5]] + StaticDefences: [[136., 36.]] + GateKeeper: [[137.25, 38.6]] + Acropolis: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 35., 109. ] ] + Pylons: [ [ 32., 109. ] ] + ThreeByThrees: [ [ 38.5, 106.5 ], [ 34.5, 105.5 ], [ 31.5, 105.5 ] ] + StaticDefences: [ [ 39., 109. ] ] + GateKeeper: [ [ 36.6, 105.6 ] ] + LowerSpawn: + FirstPylon: [ [ 141., 63. ] ] + Pylons: [ [ 144., 63. ] ] + ThreeByThrees: [ [ 137.5, 66.5 ], [ 141.5, 66.5 ], [ 144.5, 66.5 ] ] + StaticDefences: [ [ 137., 64. ] ] + GateKeeper: [ [ 139.3, 67.4 ] ] + Automaton: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 141., 139. ] ] + Pylons: [ [ 140., 142. ] ] + ThreeByThrees: [ [ 138.5, 133.5 ], [ 136.5, 137.5 ], [ 136.5, 140.5 ] ] + StaticDefences: [ [ 142., 136. ] ] + GateKeeper: [ [ 136.9, 135.6 ] ] + LowerSpawn: + FirstPylon: [ [ 43., 42. ] ] + Pylons: [ [ 44., 39. ] ] + ThreeByThrees: [ [ 45.5, 46.5 ], [ 47.5, 42.5 ], [ 47.5, 39.5 ] ] + StaticDefences: [ [ 42., 45. ] ] + GateKeeper: [ [ 47.15, 45.3 ] ] + Ephemeron: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 37., 112. ] ] + Pylons: [ [ 37., 109. ] ] + ThreeByThrees: [ [ 42.5, 114.5 ], [ 42.5, 110.5 ], [ 41.5, 107.5 ] ] + StaticDefences: [ [ 38., 115. ] ] + GateKeeper: [ [ 43.1, 112.58 ] ] + LowerSpawn: + FirstPylon: [ [ 125., 49. ] ] + Pylons: [ [ 125., 52. ] ] + ThreeByThrees: [ [ 120.5, 45.5 ], [ 120.5, 49.5 ], [ 120.5, 52.5 ] ] + StaticDefences: [ [ 125., 46. ] ] + GateKeeper: [ [ 119.7, 47.4 ] ] + Interloper: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 31., 112. ] ] + Pylons: [ [ 31., 109. ] ] + ThreeByThrees: [ [ 35.5, 114.5 ], [ 35.5, 110.5 ], [ 35.5, 107.5 ] ] + StaticDefences: [ [ 31., 115. ] ] + GateKeeper: [ [ 36.16, 112.68 ] ] + LowerSpawn: + FirstPylon: [ [ 121., 56. ] ] + Pylons: [ [ 121., 59. ] ] + ThreeByThrees: [ [ 116.5, 53.5 ], [ 116.5, 57.5 ], [ 116.5, 60.5 ] ] + StaticDefences: [ [ 121., 53. ] ] + GateKeeper: [ [ 115.78, 55.75 ] ] + Thunderbird: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 46., 106. ] ] + Pylons: [ [ 46., 103. ] ] + ThreeByThrees: [ [ 50.5, 109.5 ], [ 50.5, 105.5 ], [ 50.5, 102.5 ] ] + StaticDefences: [ [ 46., 109. ] ] + GateKeeper: [ [ 51.18, 107.67 ] ] + LowerSpawn: + FirstPylon: [ [ 144., 51 ] ] + Pylons: [ [ 144., 54. ] ] + ThreeByThrees: [ [ 139.5, 46.5 ], [ 139.5, 50.5 ], [ 139.5, 53.5 ] ] + StaticDefences: [ [ 144., 48. ] ] + GateKeeper: [ [ 138.64, 48.49 ] ] diff --git a/src/ares/config_parser.py b/src/ares/config_parser.py index 9b49d586..50040177 100644 --- a/src/ares/config_parser.py +++ b/src/ares/config_parser.py @@ -3,8 +3,6 @@ import yaml -from ares.consts import CONFIG_FILE - @dataclass class ConfigParser: @@ -22,6 +20,7 @@ class ConfigParser: ares_config_location: str user_config_location: str + config_file_name: str def parse(self) -> dict: """ @@ -33,8 +32,12 @@ def parse(self) -> dict: Internal parsed config.yml by default, or the merged internal and users config files. """ - internal_config_path: str = path.join(self.ares_config_location, CONFIG_FILE) - user_config_path: str = path.join(self.user_config_location, CONFIG_FILE) + internal_config_path: str = path.join( + self.ares_config_location, self.config_file_name + ) + user_config_path: str = path.join( + self.user_config_location, self.config_file_name + ) if path.isfile(internal_config_path): with open(internal_config_path, "r") as config_file: internal_config: dict = yaml.safe_load(config_file) @@ -51,8 +54,7 @@ def parse(self) -> dict: # there is a user config and internal config, sort out the differences return self._merge_config_files(internal_config, user_config) - @staticmethod - def _merge_config_files(internal_config: dict, user_config: dict) -> dict: + def _merge_config_files(self, internal_config: dict, user_config: dict) -> dict: """ Merge internal and user config files so we are left with just one. @@ -71,14 +73,18 @@ def _merge_config_files(internal_config: dict, user_config: dict) -> dict: Dict : A single config dictionary where user values override default values. """ - config: dict = internal_config.copy() - # iterate through internal config, and search for matching values in user config - for k, v in internal_config.items(): - value_type = type(v) - if value_type == dict and k in user_config: - config[k] = internal_config[k] | user_config[k] - - elif k in user_config and isinstance(user_config[k], value_type): - config[k] = user_config[k] - - return config + """Recursively merge override dictionary into default dictionary.""" + for key, value in user_config.items(): + if ( + isinstance(value, dict) + and key in internal_config + and isinstance(internal_config[key], dict) + ): + # Recursively merge dictionaries + internal_config[key] = self._merge_config_files( + internal_config[key], value + ) + else: + # Override or add the value + internal_config[key] = value + return internal_config diff --git a/src/ares/consts.py b/src/ares/consts.py index ee15bdf9..c4500b8e 100644 --- a/src/ares/consts.py +++ b/src/ares/consts.py @@ -19,6 +19,7 @@ BLINDING_CLOUD: str = "BlindingCloud" BOOST_BACK_TO_TOWNHALL: str = "BoostBackToTownHall" BUILD_CHOICES: str = "BuildChoices" +BUILDING_PLACEMENTS: str = "building_placements.yml" BUILDS: str = "Builds" CHAT_DEBUG: str = "ChatDebug" COMBAT: str = "Combat" @@ -142,6 +143,17 @@ """Enums""" +class BuildingPlacementOptions(str, Enum): + LOWER_SPAWN = "LowerSpawn" + UPPER_SPAWN = "UpperSpawn" + VS_ZERG_NAT_WALL = "VsZergNatWall" + FIRST_PYLON = "FirstPylon" + PYLONS = "Pylons" + THREE_BY_THREES = "ThreeByThrees" + STATIC_DEFENCES = "StaticDefences" + GATE_KEEPER = "GateKeeper" + + class BuildingSize(str, Enum): FIVE_BY_FIVE = "FIVE_BY_FIVE" THREE_BY_THREE = "THREE_BY_THREE" @@ -178,6 +190,7 @@ class BuildOrderTargetOptions(str, Enum): FOURTH = "FOURTH" MAP_CENTER = "MAP_CENTER" NAT = "NAT" + NAT_WALL = "NAT_WALL" RAMP = "RAMP" SIXTH = "SIXTH" SPAWN = "SPAWN" @@ -288,6 +301,7 @@ class ManagerRequestType(str, Enum): # PlacementManager CAN_PLACE_STRUCTURE = "CAN_PLACE_STRUCTURE" GET_PLACEMENTS_DICT = "GET_PLACEMENTS_DICT" + GET_PVZ_NAT_GATEKEEPER_POS = "GET_PVZ_NAT_GATEKEEPER_POS" REQUEST_BUILDING_PLACEMENT = "REQUEST_BUILDING_PLACEMENT" REQUEST_WARP_IN = "REQUEST_WARP_IN_SPOT" @@ -404,6 +418,7 @@ class UnitRole(str, Enum): FLANK_GROUP_TWO = "FLANK_GROUP_TWO" FLANK_GROUP_THREE = "FLANK_GROUP_THREE" GAS_STEAL_PREVENTER = "GAS_STEAL_PREVENTER" + GATE_KEEPER = "GATE_KEEPER" GATHERING = "GATHERING" # workers that are mining HARASSING = "HARASSING" # units that are harassing HARASSING_ADEPT = "HARASSING_ADEPT" diff --git a/src/ares/main.py b/src/ares/main.py index 0e5ce82a..850b9507 100644 --- a/src/ares/main.py +++ b/src/ares/main.py @@ -3,6 +3,7 @@ from typing import DefaultDict, Dict, List, Optional, Set, Tuple, Union import yaml +from config_parser import ConfigParser from cython_extensions import cy_unit_pending from loguru import logger from s2clientprotocol.raw_pb2 import Unit as RawUnit @@ -22,12 +23,12 @@ from ares.behavior_exectioner import BehaviorExecutioner from ares.behaviors.behavior import Behavior from ares.build_runner.build_order_runner import BuildOrderRunner -from ares.config_parser import ConfigParser from ares.consts import ( ADD_ONS, ADD_SHADES_ON_FRAME, ALL_STRUCTURES, CHAT_DEBUG, + CONFIG_FILE, DEBUG, DEBUG_GAME_STEP, DEBUG_OPTIONS, @@ -77,15 +78,13 @@ def __init__(self, game_step_override: Optional[int] = None): # pragma: no cove specified elsewhere """ super().__init__() - # use this Dict when compiling - # self.config: Dict = CONFIG # otherwise we use the config.yml file __ares_config_location__: str = path.realpath( path.join(getcwd(), path.dirname(__file__)) ) self.__user_config_location__: str = path.abspath(".") config_parser: ConfigParser = ConfigParser( - __ares_config_location__, self.__user_config_location__ + __ares_config_location__, self.__user_config_location__, CONFIG_FILE ) self.config = config_parser.parse() diff --git a/src/ares/managers/manager_mediator.py b/src/ares/managers/manager_mediator.py index 2619a970..bdafbe1f 100644 --- a/src/ares/managers/manager_mediator.py +++ b/src/ares/managers/manager_mediator.py @@ -1364,6 +1364,24 @@ def get_placements_dict(self, **kwargs) -> dict: ManagerName.PLACEMENT_MANAGER, ManagerRequestType.GET_PLACEMENTS_DICT ) + @property + def get_pvz_nat_gatekeeping_pos(self, **kwargs) -> Optional[Point2]: + """Get the gatekeeper position in a PvZ natural wall if available. + + WARNING: This can return `None` so your code should account for this. + + PlacementManager + + + Returns + ---------- + Optional[Point2] : + Position of gatekeeper in natural wall + """ + return self.manager_request( + ManagerName.PLACEMENT_MANAGER, ManagerRequestType.GET_PVZ_NAT_GATEKEEPER_POS + ) + def request_building_placement(self, **kwargs) -> Optional[Point2]: """Request a building placement from the precalculated building formation. @@ -1376,18 +1394,25 @@ def request_building_placement(self, **kwargs) -> Optional[Point2]: This should be a expansion location. structure_type : UnitID Structure type requested. - wall : bool, optional + first_pylon : bool (default=False) + Try to take designated first pylon if available. + static_defence : bool (default=False) + Try to take designated static defence placements if available. + wall : bool (default=False) Request a wall structure placement. Will find alternative if no wall placements available. - find_alternative : bool, optional (NOT YET IMPLEMENTED) + find_alternative : bool (default=True) If no placements available at base_location, find alternative at nearby base. - reserve_placement : bool, optional - Reserve this booking for a while, so another customer doesnt + reserve_placement : bool (default=True) + Reserve this booking for a while, so another customer doesn't request it. - within_psionic_matrix : bool, optional + within_psionic_matrix : bool (default=False) Protoss specific -> calculated position have power? - closest_to : Optional[Point2] + pylon_build_progress : float (default=1.0) + Only relevant if `within_psionic_matrix = True` + closest_to : Point2, optional + Find placement at base closest to this Returns ---------- diff --git a/src/ares/managers/placement_manager.py b/src/ares/managers/placement_manager.py index fbe410b8..0e35c933 100644 --- a/src/ares/managers/placement_manager.py +++ b/src/ares/managers/placement_manager.py @@ -1,6 +1,7 @@ import time from itertools import product from math import ceil +from os import getcwd, path from typing import ( TYPE_CHECKING, Any, @@ -13,6 +14,7 @@ ) import numpy as np +from config_parser import ConfigParser from cython_extensions import ( cy_can_place_structure, cy_distance_to_squared, @@ -32,12 +34,14 @@ from sc2.units import Units from ares.consts import ( + BUILDING_PLACEMENTS, DEBUG, DEBUG_OPTIONS, GAS_BUILDINGS, PLACEMENT, SHOW_BUILDING_FORMATION, WORKER_ON_ROUTE_TIMEOUT, + BuildingPlacementOptions, BuildingSize, ManagerName, ManagerRequestType, @@ -68,6 +72,9 @@ class PlacementManager(Manager, IManagerMediator): For convenience, this allows faster lookup of placements_dict. """ + creep_grid: np.ndarray + placement_grid: np.ndarray + pathing_grid: np.ndarray points_to_avoid_grid: np.ndarray BUILDING_SIZE_ENUM_TO_TUPLE: dict[BuildingSize, tuple[int, int]] = { BuildingSize.FIVE_BY_FIVE: (5, 5), @@ -113,6 +120,9 @@ def __init__( ManagerRequestType.GET_PLACEMENTS_DICT: lambda kwargs: ( self.placements_dict ), + ManagerRequestType.GET_PVZ_NAT_GATEKEEPER_POS: lambda kwargs: ( + self._pvz_nat_gatekeeper_pos + ), ManagerRequestType.REQUEST_BUILDING_PLACEMENT: lambda kwargs: ( self.request_building_placement(**kwargs) ), @@ -141,6 +151,17 @@ def __init__( self.warp_in_positions: set[Point2] = set() self.requested_warp_ins: list[(UnitID, Point2)] = [] + __ares_config_location__: str = path.realpath( + path.join(getcwd(), path.dirname(__file__), "..") + ) + self.__user_config_location__: str = path.abspath(".") + config_parser: ConfigParser = ConfigParser( + __ares_config_location__, self.__user_config_location__, BUILDING_PLACEMENTS + ) + + self._user_placements: dict = config_parser.parse() + self._pvz_nat_gatekeeper_pos: Optional[Point2] = None + def manager_request( self, receiver: ManagerName, @@ -201,6 +222,10 @@ def initialise(self) -> None: self.points_to_avoid_grid = np.zeros( self.ai.game_info.placement_grid.data_numpy.shape, dtype=np.uint8 ) + self.creep_grid = self.ai.state.creep.data_numpy + self.placement_grid = self.ai.game_info.placement_grid.data_numpy + # Note: use MapAnalyzers pathing grid to get rocks etc + self.pathing_grid = self.manager_mediator.get_ground_grid.astype(np.uint8).T for destructible in self.ai.destructables: if destructible.type_id in self.UNBUILDABLES: pos: Point2 = destructible.position @@ -209,6 +234,8 @@ def initialise(self) -> None: self.points_to_avoid_grid[ start_y : start_y + 2, start_x : start_x + 2 ] = 1 + # before doing any automated placement formation, add user placements + self._extract_user_placements() self.race_to_building_solver_method[self.ai.race]() finish: float = time.time() logger.info(f"Solved placement formation in {(finish - start)*1000} ms") @@ -366,6 +393,8 @@ def request_building_placement( self, base_location: Point2, structure_type: UnitID, + first_pylon: bool = False, + static_defence: bool = False, wall: bool = False, find_alternative: bool = True, reserve_placement: bool = True, @@ -382,18 +411,22 @@ def request_building_placement( This should be a expansion location. structure_type : UnitID Structure type requested. - wall : bool, optional + first_pylon : bool (default=False) + Try to take designated first pylon if available. + static_defence : bool (default=False) + Try to take designated static defence placements if available. + wall : bool (default=False) Request a wall structure placement. Will find alternative if no wall placements available. - find_alternative : bool, optional + find_alternative : bool (default=True) If no placements available at base_location, find alternative at nearby base. - reserve_placement : bool, optional + reserve_placement : bool (default=True) Reserve this booking for a while, so another customer doesn't request it. - within_psionic_matrix : bool, optional + within_psionic_matrix : bool (default=False) Protoss specific -> calculated position have power? - pylon_build_progress : float, optional (default = 1.0) + pylon_build_progress : float (default=1.0) Only relevant if `within_psionic_matrix = True` closest_to : Point2, optional Find placement at base closest to this @@ -433,6 +466,16 @@ def request_building_placement( pylon_build_progress, ) + # don't steal static def positions + if structure_type == UnitID.PYLON: + available = [ + a + for a in available + if not self.placements_dict[location][building_size][a][ + "static_defence" + ] + ] + # no available placements at base_location if len(available) == 0: if not find_alternative: @@ -455,83 +498,17 @@ def request_building_placement( break if len(available) == 0: - logger.info( - f"{self.ai.time_formatted}: No available {building_size}" - f" found anywhere on map, giving up." - ) + if self.ai.time > 60.0: + logger.info( + f"{self.ai.time_formatted}: No available {building_size}" + f" found anywhere on map, giving up." + ) return - # get closest available by default - if not closest_to: - final_placement: Point2 = min( - available, key=lambda k: cy_distance_to_squared(k, building_at_base) - ) - else: - final_placement: Point2 = min( - available, key=lambda k: cy_distance_to_squared(k, closest_to) - ) + # we have some available positions, calculate the final placement - # if wall placement is requested swap final_placement if possible - if wall: - if _available := [ - a - for a in available - if self.placements_dict[location][building_size][a]["is_wall"] - ]: - final_placement = min( - _available, - key=lambda k: cy_distance_to_squared(k, base_location), - ) - else: - final_placement = min( - available, - key=lambda k: cy_distance_to_squared( - k, self.ai.main_base_ramp.top_center - ), - ) - elif structure_type == UnitID.BUNKER: - if _available := [ - a - for a in available - if self.placements_dict[location][building_size][a]["bunker"] - ]: - final_placement = min( - _available, - key=lambda k: cy_distance_to_squared(k, building_at_base), - ) - # prioritize production pylons if they exist - elif structure_type == UnitID.PYLON: - if available_opt := [ - a - for a in available - if self.placements_dict[building_at_base][building_size][a][ - "optimal_pylon" - ] - # don't wall in, user should intentionally pass wall parameter - and not self.placements_dict[building_at_base][building_size][a][ - "is_wall" - ] - ]: - final_placement = min( - available_opt, - key=lambda k: cy_distance_to_squared(k, building_at_base), - ) - elif available := [ - a - for a in available - if self.placements_dict[building_at_base][building_size][a][ - "production_pylon" - ] - # don't wall in, user should intentionally pass wall parameter - and not self.placements_dict[building_at_base][building_size][a][ - "is_wall" - ] - ]: - final_placement = min( - available, - key=lambda k: cy_distance_to_squared(k, building_at_base), - ) - elif within_psionic_matrix: + # if we require power + if within_psionic_matrix: build_near: Point2 = building_at_base two_by_twos: dict = self.placements_dict[building_at_base][ BuildingSize.TWO_BY_TWO @@ -555,14 +532,104 @@ def request_building_placement( ] if len(close_to_pylon) < 4: build_near = optimal_pylon[0] + closest_to: Point2 = ( + base_location if not wall else self.ai.main_base_ramp.bottom_center + ) final_placement = self._find_placement_near_pylon( - available, build_near, pylon_build_progress + available, build_near, pylon_build_progress, closest_to ) if not final_placement: logger.warning( f"Can't find placement near pylon near {building_at_base}." ) return + # don't need power, all other options + else: + # let this block be the default placement + if not closest_to: + final_placement: Point2 = min( + available, + key=lambda k: cy_distance_to_squared(k, building_at_base), + ) + else: + final_placement: Point2 = min( + available, key=lambda k: cy_distance_to_squared(k, closest_to) + ) + # Now in this block, see if we want to specialize final_placement + # First Pylon + if first_pylon and ( + available_first_pylon := [ + a + for a in available + if self.placements_dict[building_at_base][building_size][a][ + "first_pylon" + ] + ] + ): + final_placement = min( + available_first_pylon, + key=lambda k: cy_distance_to_squared(k, building_at_base), + ) + + # At wall + elif wall and ( + available_wall := [ + a + for a in available + if self.placements_dict[building_at_base][building_size][a][ + "is_wall" + ] + ] + ): + final_placement = min( + available_wall, + key=lambda k: cy_distance_to_squared( + k, self.ai.main_base_ramp.bottom_center + ), + ) + + # Static Defence + elif static_defence and ( + available_static_defence := [ + a + for a in available + if self.placements_dict[building_at_base][building_size][a][ + "static_defence" + ] + ] + ): + final_placement = min( + available_static_defence, + key=lambda k: cy_distance_to_squared(k, building_at_base), + ) + + # Optimal Pylon + elif structure_type == UnitID.PYLON and ( + available_opt := [ + a + for a in available + if self.placements_dict[building_at_base][building_size][a][ + "optimal_pylon" + ] + ] + ): + final_placement = min( + available_opt, + key=lambda k: cy_distance_to_squared(k, building_at_base), + ) + + # prod pylons + elif available_prod := [ + a + for a in available + if self.placements_dict[building_at_base][building_size][a][ + "production_pylon" + ] + ]: + final_placement = min( + available_prod, + key=lambda k: cy_distance_to_squared(k, building_at_base), + ) if reserve_placement: self.worker_on_route_tracker[final_placement] = building_at_base @@ -577,6 +644,141 @@ def request_building_placement( else: logger.warning(f"No {building_size} present in placement bookkeeping.") + def _extract_user_placements(self) -> None: + if self.ai.race.name not in self._user_placements: + return + + def normalize_map_name(_map_name: str) -> str: + _map_name = _map_name.lower() + # List of suffixes to remove + suffixes = ["le", "aie"] + + # Check for suffixes at the end, with or without a preceding space + for suffix in suffixes: + if _map_name.endswith(suffix): + _map_name = _map_name[: -len(suffix)] + elif map_name.endswith(f" {suffix}"): + _map_name = _map_name[: -len(suffix) - 1] + + # Remove all spaces and lowercase the name for consistent matching + return _map_name.replace(" ", "") + + for map_name, placements in self._user_placements[self.ai.race.name].items(): + building_location_info: Optional[dict] = None + + if normalize_map_name(map_name) == normalize_map_name( + self.ai.game_info.map_name + ): + for building_type in placements: + if building_type == BuildingPlacementOptions.VS_ZERG_NAT_WALL: + + upper_spawn: bool = ( + self.ai.start_location.y + > self.ai.enemy_start_locations[0].y + ) + + if ( + upper_spawn + and BuildingPlacementOptions.UPPER_SPAWN + in placements[building_type] + ): + building_location_info = placements[building_type][ + BuildingPlacementOptions.UPPER_SPAWN + ] + elif ( + not upper_spawn + and BuildingPlacementOptions.LOWER_SPAWN + in placements[building_type] + ): + building_location_info = placements[building_type][ + BuildingPlacementOptions.LOWER_SPAWN + ] + + if not building_location_info: + continue + + el: Point2 = self.manager_mediator.get_own_nat + self.placements_dict[el] = {} + self.placements_dict[el][BuildingSize.TWO_BY_TWO] = {} + self.placements_dict[el][BuildingSize.THREE_BY_THREE] = {} + + def log_warning(pos: Point2) -> None: + logger.warning( + f"User passed building location {pos} into " + f"`building_locations.yml` but not possible to place it. " + f"This building location will be ignored." + ) + + first_pylon: Optional[Point2] = None + for building, positions in building_location_info.items(): + match building: + case BuildingPlacementOptions.FIRST_PYLON: + for p in positions: + first_pylon = Point2(p) + if not self.can_place_structure(first_pylon, UnitID.PYLON): + log_warning(first_pylon) + else: + self._add_placement_position( + BuildingSize.TWO_BY_TWO, + el, + Point2(p), + wall=True, + first_pylon=True, + add_to_avoid_grid=True, + ) + case BuildingPlacementOptions.PYLONS: + for p in positions: + pos: Point2 = Point2(p) + if not self.can_place_structure(pos, UnitID.PYLON): + log_warning(pos) + else: + self._add_placement_position( + BuildingSize.TWO_BY_TWO, + el, + pos, + wall=True, + add_to_avoid_grid=True, + production_pylon=True, + ) + case BuildingPlacementOptions.THREE_BY_THREES: + for p in positions: + pos: Point2 = Point2(p) + if not self.can_place_structure(pos, UnitID.GATEWAY): + log_warning(pos) + else: + self._add_placement_position( + BuildingSize.THREE_BY_THREE, + el, + Point2(p), + wall=True, + add_to_avoid_grid=True, + ) + case BuildingPlacementOptions.STATIC_DEFENCES: + for p in positions: + pos: Point2 = Point2(p) + if not self.can_place_structure(pos, UnitID.SHIELDBATTERY): + log_warning(pos) + else: + self._add_placement_position( + BuildingSize.TWO_BY_TWO, + el, + Point2(p), + static_defence=True, + add_to_avoid_grid=True, + ) + case BuildingPlacementOptions.GATE_KEEPER: + for p in positions: + self._pvz_nat_gatekeeper_pos = Point2(p) + + if first_pylon: + pos_x = int(first_pylon[0] - 8.0) + pos_y = int(first_pylon[1] - 8.0) + + self.points_to_avoid_grid[ + pos_y : pos_y + 16, + pos_x : pos_x + 16, + ] = 1 + def _find_potential_placements_at_base( self, building_size: BuildingSize, @@ -615,6 +817,7 @@ def _find_placement_near_pylon( available: list[Point2], base_location: Point2, pylon_build_progress: float, + closest_to: Point2, ) -> Optional[Point2]: pylons = self.manager_mediator.get_own_structures_dict[UnitID.PYLON] # first we check for ready pylons @@ -628,9 +831,7 @@ def _find_placement_near_pylon( pylon_build_progress=pylon_build_progress, ) ]: - return min( - available, key=lambda k: cy_distance_to_squared(k, base_location) - ) + return min(available, key=lambda k: cy_distance_to_squared(k, closest_to)) # then check for those in progress else: if available := [ @@ -768,6 +969,82 @@ def _make_placement_unavailable( if building_pos in self.worker_on_route_tracker: self.worker_on_route_tracker.pop(building_pos) + def _find_placements_for_base_location( + self, + el: Point2, + max_dist: int, + x_stride: int, + y_stride: int, + building_height: int, + building_width: int, + building_size: BuildingSize, + kernel_shape: tuple[int, int], + reduce_x_stride: bool = False, + drop_placement_interval: int = 0, + production_pylon: bool = False, + ) -> None: + """ + Find placements at base location using flood fill and convolution + Parameters + ---------- + el + + Returns + ------- + + """ + area_points: set[tuple[int, int]] = self.manager_mediator.get_flood_fill_area( + start_point=el, max_dist=max_dist + ) + if reduce_x_stride and x_stride >= 7 and len(area_points) < 300: + x_stride = 5 + + raw_x_bounds, raw_y_bounds = cy_get_bounding_box(area_points) + + positions = cy_find_building_locations( + kernel=np.ones(kernel_shape, dtype=np.uint8), + x_stride=x_stride, + y_stride=y_stride, + x_bounds=raw_x_bounds, + y_bounds=raw_y_bounds, + creep_grid=self.creep_grid, + placement_grid=self.placement_grid, + pathing_grid=self.pathing_grid, + points_to_avoid_grid=self.points_to_avoid_grid, + building_width=building_width, + building_height=building_height, + avoid_creep=True, + ) + + for i, pos in enumerate(positions): + x: float = pos[0] + y: float = pos[1] + point2_pos: Point2 = Point2((x, y)) + # drop some placements to avoid walling in + if ( + drop_placement_interval + and len(positions) > 6 + and i % drop_placement_interval == 0 + ): + continue + if ( + self.ai.get_terrain_height(point2_pos) == self.ai.get_terrain_height(el) + and cy_distance_to_squared( + point2_pos, self.ai.main_base_ramp.top_center + ) + > 49.0 + ): + self._add_placement_position( + building_size, el, point2_pos, production_pylon=production_pylon + ) + # move back to top left corner of 3x3, so we can add to avoid grid + avoid_x = int(x - (building_width / 2)) + avoid_y = int(y - (building_height / 2)) + self.points_to_avoid_grid[ + avoid_y : avoid_y + kernel_shape[1], + avoid_x : avoid_x + kernel_shape[0], + ] = 1 + def _solve_terran_building_formation(self): """Solve Terran building placements for every expansion location. @@ -781,12 +1058,6 @@ def _solve_terran_building_formation(self): avoids found 5x3 placements - add found locations to `self.placements_dict` """ - creep_grid: np.ndarray = self.ai.state.creep.data_numpy - placement_grid: np.ndarray = self.ai.game_info.placement_grid.data_numpy - # Note: use MapAnalyzers pathing grid to get rocks etc - pathing_grid: np.ndarray = self.manager_mediator.get_ground_grid.astype( - np.uint8 - ).T self._solve_natural_bunker() for el in self.ai.expansion_locations_list: if el not in self.placements_dict: @@ -807,95 +1078,42 @@ def _solve_terran_building_formation(self): max_dist = 22 self._calculate_terran_main_ramp_placements(el) - area_points: set[ - tuple[int, int] - ] = self.manager_mediator.get_flood_fill_area( - start_point=el, max_dist=max_dist - ) - raw_x_bounds, raw_y_bounds = cy_get_bounding_box(area_points) x_stride: int = ( 7 if el == self.ai.start_location or el == self.ai.enemy_start_locations[0] - or len(area_points) > 300 else 5 ) - - three_by_three_positions = cy_find_building_locations( - kernel=np.ones((5, 3), dtype=np.uint8), + self._find_placements_for_base_location( + el=el, + max_dist=max_dist, x_stride=x_stride, y_stride=3, - x_bounds=raw_x_bounds, - y_bounds=raw_y_bounds, - creep_grid=creep_grid, - placement_grid=placement_grid, - pathing_grid=pathing_grid, - points_to_avoid_grid=self.points_to_avoid_grid, - building_width=3, building_height=3, - avoid_creep=True, + building_width=3, + building_size=BuildingSize.THREE_BY_THREE, + kernel_shape=(5, 3), + reduce_x_stride=True, ) - for i, pos in enumerate(three_by_three_positions): - x: float = pos[0] - y: float = pos[1] - point2_pos: Point2 = Point2((x, y)) - if ( - self.ai.get_terrain_height(point2_pos) - == self.ai.get_terrain_height(el) - and cy_distance_to_squared( - point2_pos, self.ai.main_base_ramp.top_center - ) - > 49.0 - ): - self._add_placement_position( - BuildingSize.THREE_BY_THREE, el, point2_pos - ) - # move back to top left corner of 3x3, so we can add to avoid grid - avoid_x = int(x - 1.5) - avoid_y = int(y - 1.5) - self.points_to_avoid_grid[ - avoid_y : avoid_y + 3, avoid_x : avoid_x + 5 - ] = 1 - - # avoid within 7.5 distance of base location + # now avoid within 7.5 distance of base location start_x = int(el.x - 7.5) start_y = int(el.y - 7.5) self.points_to_avoid_grid[ start_y : start_y + 15, start_x : start_x + 15 ] = 1 - supply_positions = cy_find_building_locations( - kernel=np.ones((2, 2), dtype=np.uint8), + + self._find_placements_for_base_location( + el=el, + max_dist=max_dist, x_stride=2, y_stride=2, - x_bounds=raw_x_bounds, - y_bounds=raw_y_bounds, - creep_grid=creep_grid, - placement_grid=placement_grid, - pathing_grid=pathing_grid, - points_to_avoid_grid=self.points_to_avoid_grid, - building_width=2, building_height=2, - avoid_creep=True, + building_width=2, + building_size=BuildingSize.TWO_BY_TWO, + kernel_shape=(2, 2), ) - for pos in supply_positions: - x: float = pos[0] - y: float = pos[1] - point2_pos: Point2 = Point2((x, y)) - if self.ai.get_terrain_height(point2_pos) == self.ai.get_terrain_height( - el - ): - self._add_placement_position( - BuildingSize.TWO_BY_TWO, el, point2_pos - ) - # move back to top left corner of 2x2, so we can add to avoid grid - avoid_x = int(x - 1.0) - avoid_y = int(y - 1.0) - self.points_to_avoid_grid[ - avoid_y : avoid_y + 2, avoid_x : avoid_x + 2 - ] = 1 - def _solve_protoss_building_formation(self): """Solve Protoss building placements for every expansion location. @@ -910,142 +1128,57 @@ def _solve_protoss_building_formation(self): this is for supply pylons, cannons, shield batteries - add found locations to `self.placements_dict` """ - creep_grid: np.ndarray = self.ai.state.creep.data_numpy - placement_grid: np.ndarray = self.ai.game_info.placement_grid.data_numpy - # Note: use MapAnalyzers pathing grid to get rocks etc - pathing_grid: np.ndarray = self.manager_mediator.get_ground_grid.astype( - np.uint8 - ).T for el in self.ai.expansion_locations_list: - self.placements_dict[el] = {} - self.placements_dict[el][BuildingSize.TWO_BY_TWO] = {} - self.placements_dict[el][BuildingSize.THREE_BY_THREE] = {} + if el not in self.placements_dict: + self.placements_dict[el] = {} + self.placements_dict[el][BuildingSize.TWO_BY_TWO] = {} + self.placements_dict[el][BuildingSize.THREE_BY_THREE] = {} # avoid building within 9 distance of el start_x: int = int(el.x - 4.5) start_y: int = int(el.y - 4.5) self.points_to_avoid_grid[start_y : start_y + 9, start_x : start_x + 9] = 1 max_dist: int = 16 - wall_pylon: Point2 = None # calculate the wall positions first if el == self.ai.start_location: max_dist = 22 - wall_pylon = self._calculate_protoss_main_ramp_placements(el) - - area_points: set[ - tuple[int, int] - ] = self.manager_mediator.get_flood_fill_area( - start_point=el, max_dist=max_dist - ) - raw_x_bounds, raw_y_bounds = cy_get_bounding_box(area_points) + self._calculate_protoss_main_ramp_placements(el) - # find production pylon positions first - production_pylon_positions = cy_find_building_locations( - kernel=np.ones((2, 2), dtype=np.uint8), + # find prod pylons first + self._find_placements_for_base_location( + el=el, + max_dist=max_dist, x_stride=7, y_stride=7, - x_bounds=raw_x_bounds, - y_bounds=raw_y_bounds, - creep_grid=creep_grid, - placement_grid=placement_grid, - pathing_grid=pathing_grid, - points_to_avoid_grid=self.points_to_avoid_grid, - building_width=2, building_height=2, - avoid_creep=True, + building_width=2, + building_size=BuildingSize.TWO_BY_TWO, + kernel_shape=(2, 2), + production_pylon=True, ) - for pos in production_pylon_positions: - x: float = pos[0] - y: float = pos[1] - point2_pos: Point2 = Point2((x, y)) - if wall_pylon and cy_distance_to_squared(wall_pylon, point2_pos) < 56.0: - continue - if self.ai.get_terrain_height(point2_pos) == self.ai.get_terrain_height( - el - ): - self._add_placement_position( - BuildingSize.TWO_BY_TWO, el, point2_pos, True - ) - # move back to top left corner of 2x2, so we can add to avoid grid - avoid_x = int(x - 1.0) - avoid_y = int(y - 1.0) - self.points_to_avoid_grid[ - avoid_y : avoid_y + 2, avoid_x : avoid_x + 2 - ] = 1 - - # -1 from the bounding box, to avoid building 3x3 right on edge - # raw_x_bounds = (raw_x_bounds[0] - 1, raw_x_bounds[1] - 1) - # raw_y_bounds = (raw_y_bounds[0] - 1, raw_y_bounds[1] - 1) - three_by_three_positions: list = cy_find_building_locations( - kernel=np.ones((3, 3), dtype=np.uint8), + # fit in 3x3 after + self._find_placements_for_base_location( + el=el, + max_dist=max_dist, x_stride=3, y_stride=3, - x_bounds=raw_x_bounds, - y_bounds=raw_y_bounds, - creep_grid=creep_grid, - placement_grid=placement_grid, - pathing_grid=pathing_grid, - points_to_avoid_grid=self.points_to_avoid_grid, - building_width=3, building_height=3, - avoid_creep=True, + building_width=3, + building_size=BuildingSize.THREE_BY_THREE, + kernel_shape=(3, 3), ) - num_found: int = len(three_by_three_positions) - for i, pos in enumerate(three_by_three_positions): - # drop some placements to avoid walling in - if num_found > 6 and i % 4 == 0: - continue - x: float = pos[0] - y: float = pos[1] - point2_pos: Point2 = Point2((x, y)) - if self.ai.get_terrain_height(point2_pos) == self.ai.get_terrain_height( - el - ): - self._add_placement_position( - BuildingSize.THREE_BY_THREE, el, point2_pos - ) - # move back to top left corner of 3x3, so we can add to avoid grid - avoid_x = int(x - 1.5) - avoid_y = int(y - 1.5) - self.points_to_avoid_grid[ - avoid_y : avoid_y + 3, avoid_x : avoid_x + 3 - ] = 1 # find extra 2x2 last - two_by_two_positions = cy_find_building_locations( - kernel=np.ones((2, 2), dtype=np.uint8), + self._find_placements_for_base_location( + el=el, + max_dist=max_dist, x_stride=2, y_stride=3, - x_bounds=raw_x_bounds, - y_bounds=raw_y_bounds, - creep_grid=creep_grid, - placement_grid=placement_grid, - pathing_grid=pathing_grid, - points_to_avoid_grid=self.points_to_avoid_grid, - building_width=2, building_height=2, - avoid_creep=True, + building_width=2, + building_size=BuildingSize.TWO_BY_TWO, + kernel_shape=(2, 2), ) - num_found: int = len(two_by_two_positions) - for i, pos in enumerate(two_by_two_positions): - # don't add any too near to top ramp - if ( - cy_distance_to_squared(pos, self.ai.main_base_ramp.top_center) - < 30.0 - ): - continue - # drop some placements to avoid walling in - if num_found > 6 and i % 5 == 0: - continue - x: float = pos[0] - y: float = pos[1] - point2_pos: Point2 = Point2((x, y)) - if self.ai.get_terrain_height(point2_pos) == self.ai.get_terrain_height( - el - ): - self._add_placement_position( - BuildingSize.TWO_BY_TWO, el, point2_pos - ) # find optimal pylon to build around (fits most 3x3) self._find_optimal_pylon_for_base(el) @@ -1092,6 +1225,9 @@ def _add_placement_position( wall: bool = False, bunker: bool = False, optimal_pylon: bool = False, + first_pylon: bool = False, + static_defence: bool = False, + add_to_avoid_grid: bool = False, ) -> None: """Add calculated position to placements dict.""" self.placements_dict[expansion_location][building_size][position] = { @@ -1104,7 +1240,26 @@ def _add_placement_position( "production_pylon": production_pylon, "bunker": bunker, "optimal_pylon": optimal_pylon, + "first_pylon": first_pylon, + "static_defence": static_defence, } + if add_to_avoid_grid: + if building_size == BuildingSize.TWO_BY_TWO: + building_x = int(position.x - 1.0) + building_y = int(position.y - 1.0) + + self.points_to_avoid_grid[ + building_y : building_y + 2, + building_x : building_x + 2, + ] = 1 + elif building_size == BuildingSize.THREE_BY_THREE: + building_x = int(position.x - 1.5) + building_y = int(position.y - 1.5) + + self.points_to_avoid_grid[ + building_y : building_y + 3, + building_x : building_x + 3, + ] = 1 def _calculate_protoss_main_ramp_placements(self, el: Point2) -> Point2: """ @@ -1195,7 +1350,9 @@ async def _draw_building_placements(self): Debug and DebugOptions.ShowBuildingFormation should be True in config to enable. """ + for location in self.placements_dict: + three_by_three = self.placements_dict[location][BuildingSize.THREE_BY_THREE] two_by_two = self.placements_dict[location][BuildingSize.TWO_BY_TWO] z = self.ai.get_terrain_height(location) @@ -1211,6 +1368,8 @@ async def _draw_building_placements(self): pos_max = Point3((placement.x + 1.5, placement.y + 1.5, z + 2)) if info["bunker"]: colour = Point3((0, 255, 0)) + elif info["is_wall"]: + colour = Point3((255, 255, 0)) else: colour = Point3((0, 0, 255)) self.ai.client.debug_box_out(pos_min, pos_max, colour) @@ -1227,7 +1386,13 @@ async def _draw_building_placements(self): self.ai.draw_text_on_world(position, f"{placement}") pos_min = Point3((placement.x - 1.0, placement.y - 1.0, z)) pos_max = Point3((placement.x + 1.0, placement.y + 1.0, z + 1)) - if info["optimal_pylon"]: + if info["first_pylon"]: + colour = Point3((255, 255, 255)) + elif info["is_wall"]: + colour = Point3((255, 255, 0)) + elif info["static_defence"]: + colour = Point3((0, 255, 255)) + elif info["optimal_pylon"]: colour = Point3((255, 0, 0)) elif info["production_pylon"]: colour = Point3((0, 255, 0)) From 6e17526df06e85f65ada5e60a30bf0e05d0d3f50 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 21 Dec 2024 16:46:21 +0000 Subject: [PATCH 2/5] feat: added attributes to BuildStructure to make it more flexible --- src/ares/behaviors/behavior.py | 13 +--- src/ares/behaviors/macro/build_structure.py | 78 ++++++++++++++++++++- src/ares/build_runner/build_order_runner.py | 10 ++- src/ares/custom_bot_ai.py | 7 +- 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/ares/behaviors/behavior.py b/src/ares/behaviors/behavior.py index 8a6511b0..40086e4e 100644 --- a/src/ares/behaviors/behavior.py +++ b/src/ares/behaviors/behavior.py @@ -7,18 +7,7 @@ class Behavior(Protocol): - """Interface that all behaviors should adhere to. - - Notes - ----- - This is in POC stage currently, final design yet to be established. - Currently only used for `Mining`, but should support combat tasks. - Should also allow users to creat their own `Behavior` classes. - And design should allow a series of behaviors to be executed for - the same set of tags. - - Additionally, `async` methods need further thought. - """ + """Interface that all behaviors should adhere to.""" def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool: """Execute the implemented behavior. diff --git a/src/ares/behaviors/macro/build_structure.py b/src/ares/behaviors/macro/build_structure.py index ac25c690..d2b6d60a 100644 --- a/src/ares/behaviors/macro/build_structure.py +++ b/src/ares/behaviors/macro/build_structure.py @@ -1,12 +1,16 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Optional +from consts import BuildingSize +from cython_extensions.geometry import cy_distance_to_squared +from dicts.structure_to_building_size import STRUCTURE_TO_BUILDING_SIZE from sc2.data import Race from sc2.ids.unit_typeid import UnitTypeId as UnitID from sc2.position import Point2 if TYPE_CHECKING: from ares import AresBot + from ares.behaviors.macro.macro_behavior import MacroBehavior from ares.managers.manager_mediator import ManagerMediator @@ -34,23 +38,65 @@ class BuildStructure(MacroBehavior): The base location to build near. structure_id : UnitTypeId The structure type we want to build. - wall : bool + max_on_route : int (default=1) + The max number of workers on route to build this. + first_pylon : bool (default=False) + Will look for first pylon in placements dict + static_defence : bool (default=False) + Will look for static defence in placements dict + wall : bool (default=False) Find wall placement if possible. (Only main base currently supported) closest_to : Point2 (optional) Find placement at this base closest to + to_count: int (default=0) + Prevent going over this amount in total. + 0 (default) turns this check off + to_count_per_base: int (default=0) + Prevent going over this amount at this base_location. + 0 (default) turns this check off + tech_progress_check: float (optional=0.85) + Check if tech is ready before trying to build. + Setting value to 0.0 turns this check off """ base_location: Point2 structure_id: UnitID + max_on_route: int = 1 + first_pylon: bool = False + static_defence: bool = False wall: bool = False closest_to: Optional[Point2] = None + to_count: int = 0 + to_count_per_base: int = 0 + tech_progress_check: float = 0.85 def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool: assert ( ai.race != Race.Zerg ), "BuildStructure Behavior not currently supported for Zerg." + # already enough workers on route to build this + if ( + ai.not_started_but_in_building_tracker(self.structure_id) + >= self.max_on_route + ): + return False + # if `to_count` is set, see if there is already enough + if self.to_count and self._enough_existing(ai, mediator): + return False + # if we have enough at this base + if self.to_count_per_base and self._enough_existing_at_this_base(mediator): + return False + + # tech progress + if ( + self.tech_progress_check + and ai.tech_requirement_progress(self.structure_id) + < self.tech_progress_check + ): + return False + within_psionic_matrix: bool = ( ai.race == Race.Protoss and self.structure_id != UnitID.PYLON ) @@ -58,6 +104,8 @@ def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> boo if placement := mediator.request_building_placement( base_location=self.base_location, structure_type=self.structure_id, + first_pylon=self.first_pylon, + static_defence=self.static_defence, wall=self.wall, within_psionic_matrix=within_psionic_matrix, closest_to=self.closest_to, @@ -73,3 +121,31 @@ def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> boo ) return True return False + + def _enough_existing(self, ai: "AresBot", mediator: ManagerMediator) -> bool: + existing_structures = mediator.get_own_structures_dict[self.structure_id] + num_existing: int = len( + [s for s in existing_structures if s.is_ready] + ) + ai.structure_pending(self.structure_id) + return num_existing >= self.to_count + + def _enough_existing_at_this_base(self, mediator: ManagerMediator) -> bool: + placement_dict: dict = mediator.get_placements_dict + size: BuildingSize = STRUCTURE_TO_BUILDING_SIZE[self.structure_id] + potential_placements: dict[Point2:dict] = placement_dict[self.base_location][ + size + ] + taken: list[Point2] = [ + placement + for placement in potential_placements + if not potential_placements[placement]["available"] + ] + num_structures: int = 0 + for t in taken: + if [ + s + for s in mediator.get_own_structures_dict[self.structure_id] + if cy_distance_to_squared(s.position, t) < 9.0 + ]: + num_structures += 1 + return num_structures >= self.to_count_per_base diff --git a/src/ares/build_runner/build_order_runner.py b/src/ares/build_runner/build_order_runner.py index cde739a0..ab8980ea 100644 --- a/src/ares/build_runner/build_order_runner.py +++ b/src/ares/build_runner/build_order_runner.py @@ -530,7 +530,10 @@ async def get_position( and structure_type in STRUCTURE_TO_BUILDING_SIZE and structure_type != UnitID.PYLON ) - at_wall: bool = target == BuildOrderTargetOptions.RAMP + at_wall: bool = target in { + BuildOrderTargetOptions.RAMP, + BuildOrderTargetOptions.NAT_WALL, + } close_enemy_to_ramp: list[Unit] = [ e for e in self.ai.enemy_units @@ -547,6 +550,7 @@ async def get_position( base_location=base_location, structure_type=structure_type, wall=at_wall, + first_pylon=self.ai.time < 60.0, within_psionic_matrix=within_psionic_matrix, pylon_build_progress=0.5, ): @@ -650,6 +654,8 @@ def _get_target(self, target: Optional[str]) -> Point2: return self.ai.game_info.map_center case BuildOrderTargetOptions.NAT: return self.mediator.get_own_nat + case BuildOrderTargetOptions.NAT_WALL: + return self.mediator.get_own_nat case BuildOrderTargetOptions.RAMP: return self.ai.main_base_ramp.top_center case BuildOrderTargetOptions.SIXTH: @@ -686,6 +692,8 @@ def _get_position_and_supply_of_first_supply(self) -> tuple[Point2, float]: base_location=target, structure_type=self.ai.supply_type, reserve_placement=False, + first_pylon=self.ai.time < 30 and step.command == UnitID.PYLON, + wall=True, ), time, ) diff --git a/src/ares/custom_bot_ai.py b/src/ares/custom_bot_ai.py index 0e01ee5c..d4eb878b 100644 --- a/src/ares/custom_bot_ai.py +++ b/src/ares/custom_bot_ai.py @@ -111,7 +111,7 @@ def get_total_supply(units: Union[Units, list[Unit]]) -> int: ] ) - def not_started_but_in_building_tracker(self, structure_type: UnitID) -> bool: + def not_started_but_in_building_tracker(self, structure_type: UnitID) -> int: """ Figures out if worker in on route to build something, and that structure_type doesn't exist yet. @@ -124,6 +124,7 @@ def not_started_but_in_building_tracker(self, structure_type: UnitID) -> bool: ------- """ + num_in_tracker: int = 0 building_tracker: dict = self.mediator.get_building_tracker_dict for tag, info in building_tracker.items(): structure_id: UnitID = building_tracker[tag][ID] @@ -135,9 +136,9 @@ def not_started_but_in_building_tracker(self, structure_type: UnitID) -> bool: if not self.structures.filter( lambda s: cy_distance_to_squared(s.position, target.position) < 1.0 ): - return True + num_in_tracker += 1 - return False + return num_in_tracker def pending_or_complete_upgrade(self, upgrade_id: UpgradeId) -> bool: if upgrade_id in self.state.upgrades: From ef75622f0e90d3b60464351c6ca0362a4a8c4da5 Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 22 Dec 2024 15:04:40 +0000 Subject: [PATCH 3/5] refactor: move warp in logic to own manager from PlecementManager --- src/ares/consts.py | 1 + src/ares/main.py | 2 +- src/ares/managers/hub.py | 5 + src/ares/managers/manager_mediator.py | 4 +- src/ares/managers/placement_manager.py | 111 -------------- src/ares/managers/warp_in_manager.py | 197 +++++++++++++++++++++++++ 6 files changed, 206 insertions(+), 114 deletions(-) create mode 100644 src/ares/managers/warp_in_manager.py diff --git a/src/ares/consts.py b/src/ares/consts.py index c4500b8e..9b18e04c 100644 --- a/src/ares/consts.py +++ b/src/ares/consts.py @@ -395,6 +395,7 @@ class ManagerName(str, Enum): UNIT_CACHE_MANAGER = "UnitCacheManager" UNIT_MEMORY_MANAGER = "UnitMemoryManager" UNIT_ROLE_MANAGER = "UnitRoleManager" + WARP_IN_MANAGER = "WarpInManager" class UnitRole(str, Enum): diff --git a/src/ares/main.py b/src/ares/main.py index 850b9507..d17d0246 100644 --- a/src/ares/main.py +++ b/src/ares/main.py @@ -387,7 +387,7 @@ async def _after_step(self) -> int: for archon_morph_action in self._archon_morph_actions: await self._do_archon_morph(archon_morph_action) self.manager_hub.path_manager.reset_grids(self.actual_iteration) - await self.manager_hub.placement_manager.do_warp_ins() + await self.manager_hub.warp_in_manager.do_warp_ins() return await super(AresBot, self)._after_step() def register_behavior(self, behavior: Behavior) -> None: diff --git a/src/ares/managers/hub.py b/src/ares/managers/hub.py index d879c991..799bfe6f 100644 --- a/src/ares/managers/hub.py +++ b/src/ares/managers/hub.py @@ -24,6 +24,7 @@ from ares.managers.unit_cache_manager import UnitCacheManager from ares.managers.unit_memory_manager import UnitMemoryManager from ares.managers.unit_role_manager import UnitRoleManager +from ares.managers.warp_in_manager import WarpInManager if TYPE_CHECKING: from ares import AresBot @@ -166,6 +167,9 @@ def __init__( self.squad_manager: SquadManager = SquadManager( ai, config, self.manager_mediator ) + self.warp_in_manager: WarpInManager = WarpInManager( + ai, config, self.manager_mediator + ) # in order of priority self.managers: list["Manager"] = [ @@ -179,6 +183,7 @@ def __init__( self.building_manager, self.ability_tracker_manager, self.placement_manager, + self.warp_in_manager, self.enemy_to_base_manager, self.intel_manager, self.combat_sim_manager, diff --git a/src/ares/managers/manager_mediator.py b/src/ares/managers/manager_mediator.py index bdafbe1f..ef7ad5a4 100644 --- a/src/ares/managers/manager_mediator.py +++ b/src/ares/managers/manager_mediator.py @@ -1365,7 +1365,7 @@ def get_placements_dict(self, **kwargs) -> dict: ) @property - def get_pvz_nat_gatekeeping_pos(self, **kwargs) -> Optional[Point2]: + def get_pvz_nat_gatekeeping_pos(self, **kwargs) -> Union[Point2, None]: """Get the gatekeeper position in a PvZ natural wall if available. WARNING: This can return `None` so your code should account for this. @@ -1439,7 +1439,7 @@ def request_warp_in(self, **kwargs) -> None: location. """ return self.manager_request( - ManagerName.PLACEMENT_MANAGER, + ManagerName.WARP_IN_MANAGER, ManagerRequestType.REQUEST_WARP_IN, **kwargs, ) diff --git a/src/ares/managers/placement_manager.py b/src/ares/managers/placement_manager.py index 0e35c933..3db0d959 100644 --- a/src/ares/managers/placement_manager.py +++ b/src/ares/managers/placement_manager.py @@ -1,6 +1,5 @@ import time from itertools import product -from math import ceil from os import getcwd, path from typing import ( TYPE_CHECKING, @@ -21,13 +20,11 @@ cy_find_building_locations, cy_get_bounding_box, cy_pylon_matrix_covers, - cy_sorted_by_distance_to, cy_towards, ) from loguru import logger from sc2.constants import ALL_GAS from sc2.data import Race -from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId as UnitID from sc2.position import Point2, Point3 from sc2.unit import Unit @@ -45,7 +42,6 @@ BuildingSize, ManagerName, ManagerRequestType, - UnitTreeQueryType, ) from ares.dicts.structure_to_building_size import STRUCTURE_TO_BUILDING_SIZE from ares.managers.manager import Manager @@ -91,8 +87,6 @@ class PlacementManager(Manager, IManagerMediator): UnitID.UNBUILDABLEBRICKSDESTRUCTIBLE, UnitID.UNBUILDABLEROCKSDESTRUCTIBLE, } - PSIONIC_MATRIX_RANGE_PYLON: float = 6.5 - PSIONIC_MATRIX_RANGE_PRISM: float = 3.75 def __init__( self, @@ -126,9 +120,6 @@ def __init__( ManagerRequestType.REQUEST_BUILDING_PLACEMENT: lambda kwargs: ( self.request_building_placement(**kwargs) ), - ManagerRequestType.REQUEST_WARP_IN: lambda kwargs: ( - self.request_warp_in(**kwargs) - ), } # main dict where all data is organised @@ -148,8 +139,6 @@ def __init__( self.WORKER_ON_ROUTE_TIMEOUT: float = self.config[PLACEMENT][ WORKER_ON_ROUTE_TIMEOUT ] - self.warp_in_positions: set[Point2] = set() - self.requested_warp_ins: list[(UnitID, Point2)] = [] __ares_config_location__: str = path.realpath( path.join(getcwd(), path.dirname(__file__), "..") @@ -289,106 +278,6 @@ def can_place_structure( include_addon=include_addon, ) - def request_warp_in( - self, build_from: UnitID, unit_type: UnitID, target: Optional[Point2] - ) -> None: - """ - Get a warp in spot closest to target - This is intended as a simulated alternative to example: - `await self.find_placement(AbilityId.WARPGATETRAIN_ZEALOT, position)` - So prevents making query to the game client. - - Parameters - ---------- - build_from - unit_type - target - - Returns - ------- - - """ - if not target: - target = self.ai.start_location - - self.requested_warp_ins.append((build_from, unit_type, target)) - - async def do_warp_ins(self) -> None: - if not self.requested_warp_ins: - return - - power_sources: list[Unit] = ( - self.manager_mediator.get_own_structures_dict[UnitID.PYLON] - + self.manager_mediator.get_own_army_dict[UnitID.WARPPRISMPHASING] - ) - if not power_sources: - logger.warning("Requesting warp in spot, but no power sources found") - return - - for build_from, unit_type, target in self.requested_warp_ins: - size = (2, 2) if unit_type == UnitID.STALKER else (1, 1) - power_sources = cy_sorted_by_distance_to(power_sources, target) - for power_source in power_sources: - type_id: UnitID = power_source.type_id - if type_id == UnitID.PYLON: - half_psionic_range = 3 - else: - half_psionic_range = ceil(self.PSIONIC_MATRIX_RANGE_PRISM / 2) - power_source_pos: Point2 = power_source.position - - positions: list[Point2] = [ - Point2((power_source_pos.x + x, power_source_pos.y + y)) - for x in range(-half_psionic_range, half_psionic_range + 1) - for y in range(-half_psionic_range, half_psionic_range + 1) - if Point2((power_source_pos.x + x, power_source_pos.y + y)) - not in self.warp_in_positions - ] - - in_range: list[Units] = self.manager_mediator.get_units_in_range( - start_points=positions, - distances=1.75, - query_tree=UnitTreeQueryType.AllOwn, - ) - - for i, pos in enumerate(positions): - if pos in self.warp_in_positions: - continue - - if [ - u - for u in in_range[i] - if cy_distance_to_squared(u.position, pos) < 2.25 - and not u.is_flying - ]: - continue - - if cy_can_place_structure( - (int(pos.x), int(pos.y)), - size, - self.ai.state.creep.data_numpy, - self.ai.game_info.placement_grid.data_numpy, - self.manager_mediator.get_ground_grid.astype(np.uint8).T, - avoid_creep=True, - include_addon=False, - ) and cy_pylon_matrix_covers( - pos, - power_sources, - self.ai.game_info.terrain_height.data_numpy, - pylon_build_progress=1.0, - ): - ability: AbilityId = ( - AbilityId.WARPGATETRAIN_STALKER - if unit_type == UnitID.STALKER - else AbilityId.WARPGATETRAIN_ZEALOT - ) - pos = await self.ai.find_placement(ability, pos) - self.warp_in_positions.add(pos) - build_from.warp_in(unit_type, pos) - self.warp_in_positions.add(pos) - if unit_type == UnitID.STALKER: - for p in pos.neighbors8: - self.warp_in_positions.add(p) - def request_building_placement( self, base_location: Point2, diff --git a/src/ares/managers/warp_in_manager.py b/src/ares/managers/warp_in_manager.py new file mode 100644 index 00000000..10bd17b7 --- /dev/null +++ b/src/ares/managers/warp_in_manager.py @@ -0,0 +1,197 @@ +from math import ceil +from typing import TYPE_CHECKING, Any, Coroutine, DefaultDict, Optional, Union + +import numpy as np +from consts import ManagerName, ManagerRequestType, UnitTreeQueryType +from cython_extensions import ( + cy_can_place_structure, + cy_distance_to_squared, + cy_pylon_matrix_covers, + cy_sorted_by_distance_to, +) +from loguru import logger +from managers.manager import Manager +from managers.manager_mediator import IManagerMediator, ManagerMediator +from sc2.ids.ability_id import AbilityId +from sc2.ids.unit_typeid import UnitTypeId as UnitID +from sc2.position import Point2 +from sc2.unit import Unit +from sc2.units import Units + +if TYPE_CHECKING: + from ares import AresBot + + +class WarpInManager(Manager, IManagerMediator): + PSIONIC_MATRIX_RANGE_PRISM: float = 3.75 + + def __init__( + self, + ai: "AresBot", + config: dict, + mediator: ManagerMediator, + ) -> None: + """Set up the manager. + + Parameters + ---------- + ai : + Bot object that will be running the game + config : + Dictionary with the data from the configuration file + mediator : + ManagerMediator used for getting information from other managers. + """ + super(WarpInManager, self).__init__(ai, config, mediator) + + self.manager_requests_dict = { + ManagerRequestType.REQUEST_WARP_IN: lambda kwargs: ( + self.request_warp_in(**kwargs) + ), + } + + self.warp_in_positions: set[Point2] = set() + self.requested_warp_ins: list[(UnitID, Point2)] = [] + + def manager_request( + self, + receiver: ManagerName, + request: ManagerRequestType, + reason: str = None, + **kwargs, + ) -> Optional[Union[dict, DefaultDict, Coroutine[Any, Any, bool]]]: + """Fetch information from this Manager so another Manager can use it. + + Parameters + ---------- + receiver : + This Manager. + request : + What kind of request is being made + reason : + Why the reason is being made + kwargs : + Additional keyword args if needed for the specific request, as determined + by the function signature (if appropriate) + + Returns + ------- + Optional[Union[Dict, DefaultDict, Coroutine[Any, Any, bool]]] : + Everything that could possibly be returned from the Manager fits in there + + """ + return self.manager_requests_dict[request](kwargs) + + async def update(self, iteration: int) -> None: + """Update worker on route + + Bookkeeping is also updated via `on_unit_destroyed` and `on_building_started`. + + Parameters + ---------- + iteration : + The current game iteration. + + """ + self.warp_in_positions = set() + self.requested_warp_ins = [] + + def request_warp_in( + self, build_from: UnitID, unit_type: UnitID, target: Optional[Point2] + ) -> None: + """ + Get a warp in spot closest to target + This is intended as a simulated alternative to example: + `await self.find_placement(AbilityId.WARPGATETRAIN_ZEALOT, position)` + So prevents making query to the game client. + + Parameters + ---------- + build_from + unit_type + target + + Returns + ------- + + """ + if not target: + target = self.ai.start_location + + self.requested_warp_ins.append((build_from, unit_type, target)) + + async def do_warp_ins(self) -> None: + if not self.requested_warp_ins: + return + + power_sources: list[Unit] = ( + self.manager_mediator.get_own_structures_dict[UnitID.PYLON] + + self.manager_mediator.get_own_army_dict[UnitID.WARPPRISMPHASING] + ) + if not power_sources: + logger.warning("Requesting warp in spot, but no power sources found") + return + + for build_from, unit_type, target in self.requested_warp_ins: + size = (2, 2) if unit_type == UnitID.STALKER else (1, 1) + power_sources = cy_sorted_by_distance_to(power_sources, target) + for power_source in power_sources: + type_id: UnitID = power_source.type_id + if type_id == UnitID.PYLON: + half_psionic_range = 3 + else: + half_psionic_range = ceil(self.PSIONIC_MATRIX_RANGE_PRISM / 2) + power_source_pos: Point2 = power_source.position + + positions: list[Point2] = [ + Point2((power_source_pos.x + x, power_source_pos.y + y)) + for x in range(-half_psionic_range, half_psionic_range + 1) + for y in range(-half_psionic_range, half_psionic_range + 1) + if Point2((power_source_pos.x + x, power_source_pos.y + y)) + not in self.warp_in_positions + ] + + in_range: list[Units] = self.manager_mediator.get_units_in_range( + start_points=positions, + distances=1.75, + query_tree=UnitTreeQueryType.AllOwn, + ) + + for i, pos in enumerate(positions): + if pos in self.warp_in_positions: + continue + + if [ + u + for u in in_range[i] + if cy_distance_to_squared(u.position, pos) < 2.25 + and not u.is_flying + ]: + continue + + if cy_can_place_structure( + (int(pos.x), int(pos.y)), + size, + self.ai.state.creep.data_numpy, + self.ai.game_info.placement_grid.data_numpy, + self.manager_mediator.get_ground_grid.astype(np.uint8).T, + avoid_creep=True, + include_addon=False, + ) and cy_pylon_matrix_covers( + pos, + power_sources, + self.ai.game_info.terrain_height.data_numpy, + pylon_build_progress=1.0, + ): + ability: AbilityId = ( + AbilityId.WARPGATETRAIN_STALKER + if unit_type == UnitID.STALKER + else AbilityId.WARPGATETRAIN_ZEALOT + ) + pos = await self.ai.find_placement(ability, pos) + self.warp_in_positions.add(pos) + build_from.warp_in(unit_type, pos) + self.warp_in_positions.add(pos) + if unit_type == UnitID.STALKER: + for p in pos.neighbors8: + self.warp_in_positions.add(p) From 56385aff4de61e2f75c5618f3f8be8b6fee9ca1a Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 22 Dec 2024 15:06:08 +0000 Subject: [PATCH 4/5] docs: add custom building placement docs --- docs/tutorials/build_runner.md | 1 + docs/tutorials/custom_building_placements.md | 292 +++++++++++++++++++ mkdocs.yml | 3 +- 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/custom_building_placements.md diff --git a/docs/tutorials/build_runner.md b/docs/tutorials/build_runner.md index 453983a9..b0474a3b 100644 --- a/docs/tutorials/build_runner.md +++ b/docs/tutorials/build_runner.md @@ -126,6 +126,7 @@ class BuildOrderTargetOptions(str, Enum): FOURTH = "FOURTH" MAP_CENTER = "MAP_CENTER" NAT = "NAT" + NAT_WALL = "NAT_WALL" RAMP = "RAMP" SIXTH = "SIXTH" SPAWN = "SPAWN" diff --git a/docs/tutorials/custom_building_placements.md b/docs/tutorials/custom_building_placements.md new file mode 100644 index 00000000..2abd470e --- /dev/null +++ b/docs/tutorials/custom_building_placements.md @@ -0,0 +1,292 @@ +Although ares-sc2 automatically calculates building formations for all base locations, +there are situations where precise placement is critical, and custom-building layouts +are preferred. To address this, ares-sc2 allows users to specify custom-building positions, +which are seamlessly integrated into its placement calculations. These custom placements +are fully compatible with core `ares-sc2` features, such as the Build Runner, `BuildStructure ` +behavior, and direct interactions with the building tracker via the `ManagerMediator`. +Additionally, the system ensures that standard placements within a base location adapt +to account for user-defined custom positions. + +At present, custom placements are supported exclusively for Protoss vs. Zerg natural wall setups. +Support for additional scenarios will be introduced in future updates. + +## Defining custom placements +Create a file in the root of your bot folder names `building_placements.yml`, you should enter placements +into this file like below. + +```yml +Protoss: + AbyssalReef: + VsZergNatWall: + UpperSpawn: + FirstPylon: [[64., 105.]] + Pylons: [[63., 112.]] + ThreeByThrees: [[68.5, 109.5], [66.5, 106.5], [60.5, 106.5]] + StaticDefences: [[64., 110.]] + GateKeeper: [[62.25, 105.86]] + LowerSpawn: + FirstPylon: [[136., 39.]] + Pylons: [[137., 32.]] + ThreeByThrees: [[131.5, 34.5], [133.5, 37.5], [139.5, 37.5]] + StaticDefences: [[136., 36.]] + GateKeeper: [[137.25, 38.6]] + Acropolis: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 35., 109. ] ] + Pylons: [ [ 32., 109. ] ] + ThreeByThrees: [ [ 38.5, 106.5 ], [ 34.5, 105.5 ], [ 31.5, 105.5 ] ] + StaticDefences: [ [ 39., 109. ] ] + GateKeeper: [ [ 36.6, 105.6 ] ] + LowerSpawn: + FirstPylon: [ [ 141., 63. ] ] + Pylons: [ [ 144., 63. ] ] + ThreeByThrees: [ [ 137.5, 66.5 ], [ 141.5, 66.5 ], [ 144.5, 66.5 ] ] + StaticDefences: [ [ 137., 64. ] ] + GateKeeper: [ [ 139.3, 67.4 ] ] + Automaton: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 141., 139. ] ] + Pylons: [ [ 140., 142. ] ] + ThreeByThrees: [ [ 138.5, 133.5 ], [ 136.5, 137.5 ], [ 136.5, 140.5 ] ] + StaticDefences: [ [ 142., 136. ] ] + GateKeeper: [ [ 136.9, 135.6 ] ] + LowerSpawn: + FirstPylon: [ [ 43., 42. ] ] + Pylons: [ [ 44., 39. ] ] + ThreeByThrees: [ [ 45.5, 46.5 ], [ 47.5, 42.5 ], [ 47.5, 39.5 ] ] + StaticDefences: [ [ 42., 45. ] ] + GateKeeper: [ [ 47.15, 45.3 ] ] + Ephemeron: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 37., 112. ] ] + Pylons: [ [ 37., 109. ] ] + ThreeByThrees: [ [ 42.5, 114.5 ], [ 42.5, 110.5 ], [ 41.5, 107.5 ] ] + StaticDefences: [ [ 38., 115. ] ] + GateKeeper: [ [ 43.1, 112.58 ] ] + LowerSpawn: + FirstPylon: [ [ 125., 49. ] ] + Pylons: [ [ 125., 52. ] ] + ThreeByThrees: [ [ 120.5, 45.5 ], [ 120.5, 49.5 ], [ 120.5, 52.5 ] ] + StaticDefences: [ [ 125., 46. ] ] + GateKeeper: [ [ 119.7, 47.4 ] ] + Interloper: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 31., 112. ] ] + Pylons: [ [ 31., 109. ] ] + ThreeByThrees: [ [ 35.5, 114.5 ], [ 35.5, 110.5 ], [ 35.5, 107.5 ] ] + StaticDefences: [ [ 31., 115. ] ] + GateKeeper: [ [ 36.16, 112.68 ] ] + LowerSpawn: + FirstPylon: [ [ 121., 56. ] ] + Pylons: [ [ 121., 59. ] ] + ThreeByThrees: [ [ 116.5, 53.5 ], [ 116.5, 57.5 ], [ 116.5, 60.5 ] ] + StaticDefences: [ [ 121., 53. ] ] + GateKeeper: [ [ 115.78, 55.75 ] ] + Thunderbird: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 46., 106. ] ] + Pylons: [ [ 46., 103. ] ] + ThreeByThrees: [ [ 50.5, 109.5 ], [ 50.5, 105.5 ], [ 50.5, 102.5 ] ] + StaticDefences: [ [ 46., 109. ] ] + GateKeeper: [ [ 51.18, 107.67 ] ] + LowerSpawn: + FirstPylon: [ [ 144., 51 ] ] + Pylons: [ [ 144., 54. ] ] + ThreeByThrees: [ [ 139.5, 46.5 ], [ 139.5, 50.5 ], [ 139.5, 53.5 ] ] + StaticDefences: [ [ 144., 48. ] ] + GateKeeper: [ [ 138.64, 48.49 ] ] + +``` + +The values shown above are the default settings in ares, so there's no need to +create your own file if you're satisfied with them. However, you can customize +these settings by creating your own building_placements.yml file and specifying +only the elements you wish to change. ares-sc2 will automatically prioritize +your custom placements and fill in any missing elements with the default values. + +This is an example contents of a `building_placements.yml` +file where the first pylon position on Thunderbird is tweaked. +```yml + Thunderbird: + VsZergNatWall: + UpperSpawn: + FirstPylon: [ [ 47., 107. ] ] + LowerSpawn: + FirstPylon: [ [ 143., 52 ] ] +``` + +When creating your building placements file, ensure the keys are spelled correctly and match the example +above. Internally `ares-sc2` checks an `Enum` similar to this when parsing the file: +```python +class BuildingPlacementOptions(str, Enum): + LOWER_SPAWN = "LowerSpawn" + UPPER_SPAWN = "UpperSpawn" + VS_ZERG_NAT_WALL = "VsZergNatWall" + FIRST_PYLON = "FirstPylon" + PYLONS = "Pylons" + THREE_BY_THREES = "ThreeByThrees" + STATIC_DEFENCES = "StaticDefences" + GATE_KEEPER = "GateKeeper" +``` + +### Providing impossible placements +`ares-sc2` validates your placements before adding them internally. If an invalid placement is detected, +an error message will be logged, but your bot will continue running as normal. If your placements +aren’t working as expected, be sure to check the logs for more details. + +## Retrieve the gate keeper placement +In Protoss vs Zerg this is the gap in the natural wall that is usually blocked by a +gateway unit. Keep in mind this could be `None` if no position is provided for the +current map. + +```python +nat_wall_gatekeeper_pos: Union[Point2, None] = self.mediator.get_pvz_nat_gatekeeping_pos +``` + +## Using custom placements with ares +There are several ways these placements can be utilized. + +### Via the BuildRunner +See [build Runner tutorial](../tutorials/build_runner.md) if you're unfamiliar. + +Below is an example of a valid build order that places structures at the natural wall. +To specify that a structure should use your custom natural wall placements, simply +add `@ nat_wall` when declaring a build step. If the map has no custom placements or +all available positions are already taken, the build runner will automatically find a +suitable alternative nearby. + +```yml +UseData: True +# How should we choose a build? Cycle is the only option for now +BuildSelection: Cycle +# For each Race / Opponent ID choose a build selection +BuildChoices: + # test_123 is active if Debug: True (set via a `config.yml` file) + test_123: + BotName: Test + Cycle: + - NatWall + + Protoss: + BotName: ProtossRace + Cycle: + - NatWall + + Random: + BotName: RandomRace + Cycle: + - NatWall + + Terran: + BotName: TerranRace + Cycle: + - NatWall + + Zerg: + BotName: ZergRace + Cycle: + - NatWall + + +Builds: + NatWall: + ConstantWorkerProductionTill: 44 + AutoSupplyAtSupply: 23 + OpeningBuildOrder: + - 14 pylon @ nat_wall + - 15 gate @ nat_wall + - 16 gate @ nat_wall + - 16 core @ nat_wall + - 16 pylon @ nat_wall + - 16 shieldbattery @ nat_wall +``` + +### `BuildStructure` behavior +You can build wall structures within your own bot logic via the +[`BuildStructure` behavior](../api_reference/behaviors/macro_behaviors.md#ares.behaviors.macro.build_structure.BuildStructure). +If wall placements are not available this will look for a closely alternative. Ensure +`base_location=self.mediator.get_own_nat` to ensure natural wall is found. +See example code: + +```python +from sc2.ids.unit_typeid import UnitTypeId + +self.register_behavior( + BuildStructure( + base_location=self.mediator.get_own_nat, + structure_id=UnitTypeId.GATEWAY, + wall=True, + to_count_per_base=2 + ) +) +self.register_behavior( + BuildStructure( + base_location=self.mediator.get_own_nat, + structure_id=UnitTypeId.CYBERNETICSCORE, + wall=True, + to_count_per_base=1 + ) +) +self.register_behavior( + BuildStructure( + base_location=self.mediator.get_own_nat, + structure_id=UnitTypeId.PYLON, + wall=True, + to_count_per_base=2 + ) +) +self.register_behavior( + BuildStructure( + base_location=self.mediator.get_own_nat, + structure_id=UnitTypeId.SHIELDBATTERY, + wall=True, + to_count_per_base=1 + ) +) +``` + +### Via the `ManagerMediator` +For more customized control you can interact with the wall placements via the `mediator`. See +some examples below. + +Get the first pylon placement without reserving placement in the building tracker: + +```python +from sc2.ids.unit_typeid import UnitTypeId + +if placement := mediator.request_building_placement( + base_location=self.mediator.get_own_nat, + structure_type=UnitTypeId.PYLON, + first_pylon=self.first_pylon, + reserve_placement=False + ): + pass +``` + +Work directly with the raw data, example here gets the natural wall placements. + +```python +from ares.consts import BuildingSize +from sc2.position import Point2 + +placements_dict: dict[Point2, dict[BuildingSize, dict]] = self.mediator.get_placements_dict +natural_placements: dict[BuildingSize, dict] = placements_dict[self.mediator.get_own_nat] + +two_by_twos_at_wall: list[Point2] = [ + placement + for placement in natural_placements[BuildingSize.TWO_BY_TWO] + if natural_placements[BuildingSize.TWO_BY_TWO][placement]["is_wall"] + ] + +three_by_threes_at_wall: list[Point2] = [ + placement + for placement in natural_placements[BuildingSize.TWO_BY_TWO] + if natural_placements[BuildingSize.THREE_BY_THREE][placement]["is_wall"] + ] +``` + diff --git a/mkdocs.yml b/mkdocs.yml index f45f4ed7..fd770701 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,8 +37,9 @@ nav: - Build Runner: tutorials/build_runner.md - Chat Debug: tutorials/chat_debug.md - Combat Maneuver Example: tutorials/combat_maneuver_example.md - - Creating Custom Behaviors: tutorials/custom_behaviors.md - Config File: tutorials/config_file.md + - Creating Custom Behaviors: tutorials/custom_behaviors.md + - Custom Building Placements: tutorials/custom_building_placements.md - Gotchas: tutorials/gotchas.md - Influence and Pathing: tutorials/influence_and_pathing.md - Managing Production: tutorials/managing_production.md From 8f47248eb6fe4a7be967fce2e251b4f2a3b0d1a6 Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 22 Dec 2024 15:13:03 +0000 Subject: [PATCH 5/5] fix: correct imports --- src/ares/behaviors/macro/build_structure.py | 5 +++-- src/ares/main.py | 2 +- src/ares/managers/placement_manager.py | 2 +- src/ares/managers/warp_in_manager.py | 7 ++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ares/behaviors/macro/build_structure.py b/src/ares/behaviors/macro/build_structure.py index d2b6d60a..8521b0fa 100644 --- a/src/ares/behaviors/macro/build_structure.py +++ b/src/ares/behaviors/macro/build_structure.py @@ -1,13 +1,14 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Optional -from consts import BuildingSize from cython_extensions.geometry import cy_distance_to_squared -from dicts.structure_to_building_size import STRUCTURE_TO_BUILDING_SIZE from sc2.data import Race from sc2.ids.unit_typeid import UnitTypeId as UnitID from sc2.position import Point2 +from ares.consts import BuildingSize +from ares.dicts.structure_to_building_size import STRUCTURE_TO_BUILDING_SIZE + if TYPE_CHECKING: from ares import AresBot diff --git a/src/ares/main.py b/src/ares/main.py index d17d0246..0a8fa1b8 100644 --- a/src/ares/main.py +++ b/src/ares/main.py @@ -3,7 +3,6 @@ from typing import DefaultDict, Dict, List, Optional, Set, Tuple, Union import yaml -from config_parser import ConfigParser from cython_extensions import cy_unit_pending from loguru import logger from s2clientprotocol.raw_pb2 import Unit as RawUnit @@ -23,6 +22,7 @@ from ares.behavior_exectioner import BehaviorExecutioner from ares.behaviors.behavior import Behavior from ares.build_runner.build_order_runner import BuildOrderRunner +from ares.config_parser import ConfigParser from ares.consts import ( ADD_ONS, ADD_SHADES_ON_FRAME, diff --git a/src/ares/managers/placement_manager.py b/src/ares/managers/placement_manager.py index 3db0d959..bf87a28b 100644 --- a/src/ares/managers/placement_manager.py +++ b/src/ares/managers/placement_manager.py @@ -13,7 +13,6 @@ ) import numpy as np -from config_parser import ConfigParser from cython_extensions import ( cy_can_place_structure, cy_distance_to_squared, @@ -30,6 +29,7 @@ from sc2.unit import Unit from sc2.units import Units +from ares.config_parser import ConfigParser from ares.consts import ( BUILDING_PLACEMENTS, DEBUG, diff --git a/src/ares/managers/warp_in_manager.py b/src/ares/managers/warp_in_manager.py index 10bd17b7..501b9ed3 100644 --- a/src/ares/managers/warp_in_manager.py +++ b/src/ares/managers/warp_in_manager.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Any, Coroutine, DefaultDict, Optional, Union import numpy as np -from consts import ManagerName, ManagerRequestType, UnitTreeQueryType from cython_extensions import ( cy_can_place_structure, cy_distance_to_squared, @@ -10,14 +9,16 @@ cy_sorted_by_distance_to, ) from loguru import logger -from managers.manager import Manager -from managers.manager_mediator import IManagerMediator, ManagerMediator from sc2.ids.ability_id import AbilityId from sc2.ids.unit_typeid import UnitTypeId as UnitID from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units +from ares.consts import ManagerName, ManagerRequestType, UnitTreeQueryType +from ares.managers.manager import Manager +from ares.managers.manager_mediator import IManagerMediator, ManagerMediator + if TYPE_CHECKING: from ares import AresBot