diff --git a/.github/workflows/build_on_setup.yml b/.github/workflows/build_on_setup.yml index 7f56d1fb..fd0ef55d 100644 --- a/.github/workflows/build_on_setup.yml +++ b/.github/workflows/build_on_setup.yml @@ -2,7 +2,7 @@ name: BuildOnSetup on: push: - branches: [ master, dev ] + branches: [ master, dev, Base ] pull_request: branches: [ master, dev ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff2f6be..648c60c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,22 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -### [0.0.51](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.50...v0.0.51) (2020-08-14) +### [0.0.52](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.51...v0.0.52) (2020-08-15) + +### ⚠ BREAKING CHANGES + +* Region is now a Child of Polygon (Refactor) + +### Bug Fixes +* regions and ramps now set each other correctly + +* mapdata test for plotting ([b987fb6](https://github.com/eladyaniv01/SC2MapAnalysis/commit/b987fb6c29863cf57b30abfa5dad3b152456bcab)) + +* Base (#65) ([209d6d1](https://github.com/eladyaniv01/SC2MapAnalysis/commit/209d6d1c065893f98ce6bbfaeb34ab38b74e41a9)), closes [#65](https://github.com/eladyaniv01/SC2MapAnalysis/issues/65) [#64](https://github.com/eladyaniv01/SC2MapAnalysis/issues/64) + + +### [0.0.51](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.50...v0.0.51) (2020-08-14) ### Features diff --git a/MapAnalyzer/Debugger.py b/MapAnalyzer/Debugger.py index 326fffaf..5aa6afc9 100644 --- a/MapAnalyzer/Debugger.py +++ b/MapAnalyzer/Debugger.py @@ -8,25 +8,28 @@ from loguru import logger from numpy import int64, ndarray -from .constants import COLORS, LOG_FORMAT +from .constants import COLORS, LOG_FORMAT, LOG_MODULE if TYPE_CHECKING: from MapAnalyzer.MapData import MapData class LogFilter: - def __init__(self, module_name: str) -> None: + def __init__(self, module_name: str, level="ERROR") -> None: self.module_name = module_name + self.level = level def __call__(self, record: Dict[str, Any]) -> bool: - # return True + levelno = logger.level(self.level).no if self.module_name.lower() in record["name"].lower() or 'main' in record["name"].lower(): - return True + return record["level"].no >= levelno return False class MapAnalyzerDebugger: - """""" + """ + MapAnalyzerDebugger + """ def __init__(self, map_data: "MapData", loglevel: str = "ERROR") -> None: self.map_data = map_data @@ -34,7 +37,7 @@ def __init__(self, map_data: "MapData", loglevel: str = "ERROR") -> None: self.warnings.filterwarnings('ignore', category=DeprecationWarning) self.warnings.filterwarnings('ignore', category=RuntimeWarning) self.logger = logger - self.log_filter = LogFilter("MapAnalyzer") + self.log_filter = LogFilter(module_name=LOG_MODULE, level=loglevel) self.logger.remove() self.log_format = LOG_FORMAT self.logger.add(sys.stderr, format=self.log_format, filter=self.log_filter) @@ -66,10 +69,10 @@ def plot_regions(self, fontdict: Dict[str, Union[str, int]]) -> None: import matplotlib.pyplot as plt for lbl, reg in self.map_data.regions.items(): c = COLORS[lbl] - fontdict["color"] = c + fontdict["color"] = 'black' fontdict["backgroundcolor"] = 'black' - if c == 'black': - fontdict["backgroundcolor"] = 'white' + # if c == 'black': + # fontdict["backgroundcolor"] = 'white' plt.text( reg.center[0], reg.center[1], @@ -78,9 +81,9 @@ def plot_regions(self, fontdict: Dict[str, Union[str, int]]) -> None: fontdict=fontdict, ) # random color for each perimeter - x, y = zip(*reg.polygon.perimeter_points) + x, y = zip(*reg.perimeter_points) plt.scatter(x, y, c=c, marker="1", s=300) - for corner in reg.polygon.corner_points: + for corner in reg.corner_points: plt.scatter(corner[0], corner[1], marker="v", c="red", s=150) def plot_vision_blockers(self) -> None: diff --git a/MapAnalyzer/MapData.py b/MapAnalyzer/MapData.py index 8a319f3b..412e5ace 100644 --- a/MapAnalyzer/MapData.py +++ b/MapAnalyzer/MapData.py @@ -249,7 +249,7 @@ def where_all( point = int(point[0]), int(point[1]) for region in self.regions.values(): - if region.inside_p(point): + if region.is_inside_point(point): results.append(region) for choke in self.map_chokes: if choke.is_inside_point(point): @@ -275,7 +275,7 @@ def where( point = int(point[0]), int(point[1]) for region in self.regions.values(): - if region.inside_p(point): + if region.is_inside_point(point): return region for choke in self.map_chokes: if choke.is_inside_point(point): @@ -356,28 +356,22 @@ def compile_map(self) -> None: self._calc_grid() self._calc_regions() self._calc_vision_blockers() + self._set_map_ramps() self._calc_chokes() self._clean_polys() + for poly in self.polygons: poly.calc_areas() - - @staticmethod - def _clean_ramps(region: Region) -> None: - """ utility function to remove over populated ramps """ - for mramp in region.region_ramps: - if len(mramp.regions) < 2: - region.region_ramps.remove(mramp) + for ramp in self.map_ramps: + ramp.set_regions() def _calc_grid(self) -> None: """ converting the placement grid to our own kind of grid""" # cleaning the grid and then searching for 2x2 patterned regions grid = binary_fill_holes(self.placement_arr).astype(int) # for our grid, mineral walls are considered as a barrier between regions - correct_blockers = [] for point in self.resource_blockers: grid[int(point[0])][int(point[1])] = 0 - if point not in self.resource_blockers: - correct_blockers.append(point) for n in point.neighbors4: point_ = Point2((n.rounded[0], n.rounded[1])) if point_[0] < grid.shape[1] and point_[1] < grid.shape[0]: @@ -409,36 +403,7 @@ def _set_map_ramps(self): array=self.points_to_numpy_array(r.points)) for r in self.bot.game_info.map_ramps] - def _calc_ramps(self, region: Region) -> None: - """ - probably the most expensive operation other than plotting , need to optimize - """ - if len(self.map_ramps) == 0: - self._set_map_ramps() - - ramp_nodes = self.get_ramp_nodes() - perimeter_nodes = region.polygon.perimeter_points - result_ramp_indexes = list(set([self.closest_node_idx(n, ramp_nodes) for n in perimeter_nodes])) - - for rn in result_ramp_indexes: - # and distance from perimeter is less than ? - ramp = self.get_ramp(node=ramp_nodes[rn]) - - """for ramp in map ramps if ramp exists, append the regions if not, create new one""" - if region not in ramp.areas: - ramp.areas.append(region) - region.region_ramps.append(ramp) - ramps = [] - - for ramp in region.region_ramps: - for p in region.polygon.perimeter_points: - if self.ramp_close_enough(ramp, p, n=8): - ramps.append(ramp) - ramps = list(set(ramps)) - region.region_ramps.extend(ramps) - region.region_ramps = list(set(region.region_ramps)) - # self._clean_ramps(region) def _calc_vision_blockers(self) -> None: """ @@ -497,6 +462,8 @@ def _calc_regions(self) -> None: """ # some areas are with area of 1, 2 ,5 these are not what we want, # so we filter those out + # if len(self.map_ramps) == 0: + # self._set_map_ramps() pre_regions = {} for i in range(len(self.regions_labels)): region = Region( @@ -513,7 +480,7 @@ def _calc_regions(self) -> None: if self.max_region_area > region.get_area > self.min_region_area: region.label = j self.regions[j] = region - self._calc_ramps(region=region) + # region.calc_ramps() j += 1 """Plot methods""" diff --git a/MapAnalyzer/Pather.py b/MapAnalyzer/Pather.py index 581d69bd..4a1d3ba0 100644 --- a/MapAnalyzer/Pather.py +++ b/MapAnalyzer/Pather.py @@ -1,4 +1,3 @@ -from functools import lru_cache from typing import Optional, Tuple, TYPE_CHECKING import numpy as np @@ -46,7 +45,9 @@ def _add_non_pathables_ground(self, grid: ndarray, include_destructables: bool = nonpathables.extend(self.map_data.mineral_fields) for obj in nonpathables: radius = NONPATHABLE_RADIUS - grid = self.add_influence(p=obj.position, r=radius * obj.radius, arr=grid, weight=np.inf) + if 'mineral' in obj.name.lower(): + radius = NONPATHABLE_RADIUS * 1.5 + grid = self.add_influence(p=obj.position.rounded, r=radius * obj.radius, arr=grid, weight=np.inf) for pos in self.map_data.resource_blockers: radius = RESOURCE_BLOCKER_RADIUS grid = self.add_influence(p=pos, r=radius, arr=grid, weight=np.inf) @@ -57,11 +58,9 @@ def _add_non_pathables_ground(self, grid: ndarray, include_destructables: bool = self.add_influence(p=rock.position, r=1 * rock.radius, arr=grid, weight=np.inf) return grid - @lru_cache() def get_base_pathing_grid(self) -> ndarray: return np.fmax(self.map_data.path_arr, self.map_data.placement_arr).T - @lru_cache() def get_climber_grid(self, default_weight: int = 1, include_destructables: bool = True) -> ndarray: """Grid for units like reaper / colossus """ grid = self._climber_grid.copy() @@ -69,7 +68,6 @@ def get_climber_grid(self, default_weight: int = 1, include_destructables: bool grid = self._add_non_pathables_ground(grid=grid, include_destructables=include_destructables) return grid - @lru_cache() def get_clean_air_grid(self, default_weight: int = 1): clean_air_grid = np.ones(shape=self.map_data.path_arr.shape).astype(np.float32).T if default_weight == 1: @@ -77,7 +75,6 @@ def get_clean_air_grid(self, default_weight: int = 1): else: return np.where(clean_air_grid == 1, default_weight, 0) - @lru_cache() def get_air_vs_ground_grid(self, default_weight: int): grid = np.fmin(self.map_data.path_arr, self.map_data.placement_arr) air_vs_ground_grid = np.where(grid == 0, 1, default_weight).astype(np.float32) @@ -113,6 +110,7 @@ def add_influence(self, p: Tuple[int, int], r: int, arr: ndarray, weight: int = if len(ri) == 0 or len(ci) == 0: # this happens when the center point is near map edge, and the radius added goes beyond the edge self.map_data.logger.debug(OutOfBoundsException(p)) + # self.map_data.logger.trace() return arr def in_bounds_ci(x): diff --git a/MapAnalyzer/Polygon.py b/MapAnalyzer/Polygon.py index 07731852..f7cd35c5 100644 --- a/MapAnalyzer/Polygon.py +++ b/MapAnalyzer/Polygon.py @@ -18,13 +18,13 @@ def __init__(self, polygon): self.points = None @property - def free_pct(self): + def free_pct(self) -> float: if self.points is None: self.polygon.map_data.logger.warning("BuildablePoints needs to update first") self.update() return len(self.points) / len(self.polygon.points) - def update(self): + def update(self) -> None: parr = self.polygon.map_data.points_to_numpy_array(self.polygon.points) [self.polygon.map_data.add_influence(p=(unit.position.x, unit.position.y), r=unit.radius, arr=parr, safe=False) for unit in @@ -66,14 +66,14 @@ def __init__(self, map_data: "MapData", array: ndarray) -> None: # pragma: no c self.indices = self.map_data.points_to_indices(self.points) self.map_data.polygons.append(self) self._buildable_points = BuildablePoints(polygon=self) + # self.calc_areas() @property - def buildable_points(self): + def buildable_points(self) -> BuildablePoints: self._buildable_points.update() return self._buildable_points @property - @lru_cache() def regions(self) -> List["Region"]: from MapAnalyzer.Region import Region if len(self.areas) > 0: @@ -81,7 +81,17 @@ def regions(self) -> List["Region"]: return [] def calc_areas(self) -> None: - pass + # this method uses where_all which means + # it should be called at the end of the map compilation when areas are populated + points = [min(self.points), max(self.points)] + areas = self.areas + for point in points: + point = int(point[0]), int(point[1]) + new_areas = self.map_data.where_all(point) + if self in new_areas: + new_areas.pop(new_areas.index(self)) + areas.extend(new_areas) + self.areas = list(set(areas)) def plot(self, testing: bool = False) -> None: # pragma: no cover """ @@ -150,7 +160,7 @@ def center(self) -> Point2: cm = self.map_data.closest_towards_point(points=self.clean_points, target=center_of_mass(self.array)) return cm - @lru_cache(100) + @lru_cache() def is_inside_point(self, point: Union[Point2, tuple]) -> bool: """ is_inside_point @@ -161,7 +171,7 @@ def is_inside_point(self, point: Union[Point2, tuple]) -> bool: return True return False - @lru_cache(100) + @lru_cache() def is_inside_indices( self, point: Union[Point2, tuple] ) -> bool: # pragma: no cover diff --git a/MapAnalyzer/Region.py b/MapAnalyzer/Region.py index 8b1eb5c5..4a29acac 100644 --- a/MapAnalyzer/Region.py +++ b/MapAnalyzer/Region.py @@ -1,20 +1,16 @@ -from functools import lru_cache -from typing import List, TYPE_CHECKING, Union +from typing import List, TYPE_CHECKING import numpy as np from sc2.position import Point2 from MapAnalyzer.Polygon import Polygon +from MapAnalyzer.constructs import MDRamp if TYPE_CHECKING: from MapAnalyzer import MapData -class Region: - """ - Region DocString - """ - +class Region(Polygon): def __init__( self, map_data: 'MapData', @@ -22,40 +18,30 @@ def __init__( label: int, map_expansions: List[Point2], ) -> None: + super().__init__(map_data=map_data, array=array) self.map_data = map_data self.array = array self.label = label - - self.polygon = Polygon(map_data=self.map_data, array=self.array) # for constructor - self.polygon.areas.append(self) - self.polygon.is_region = True + self.is_region = True self.bases = [ base for base in map_expansions - if self.polygon.is_inside_point((base.rounded[0], base.rounded[1])) - ] - self.region_ramps = [] # will be set later by mapdata + if self.is_inside_point((base.rounded[0], base.rounded[1])) + ] # will be set later by mapdata self.region_vision_blockers = [] # will be set later by mapdata self.region_vb = [] self.region_chokes = [] @property - def buildable_points(self): - return self.polygon.buildable_points - - @property - def center(self) -> Point2: - """ - center - """ - return self.polygon.center + def region_ramps(self) -> List[MDRamp]: + return [r for r in self.areas if r.is_ramp] @property def corners(self) -> List[Point2]: """ corners """ - return self.polygon.corner_points + return self.corner_points def plot_perimeter(self, self_only: bool = True) -> None: """ @@ -65,7 +51,7 @@ def plot_perimeter(self, self_only: bool = True) -> None: plt.style.use("ggplot") - x, y = zip(*self.polygon.perimeter) # reversing for "lower" origin + x, y = zip(*self.perimeter) plt.scatter(x, y) plt.title(f"Region {self.label}") if self_only: # pragma: no cover @@ -74,7 +60,7 @@ def plot_perimeter(self, self_only: bool = True) -> None: def _plot_corners(self) -> None: import matplotlib.pyplot as plt plt.style.use("ggplot") - for corner in self.polygon.corner_points: + for corner in self.corner_points: plt.scatter(corner[0], corner[1], marker="v", c="red", s=150) def _plot_ramps(self) -> None: @@ -103,7 +89,7 @@ def _plot_vision_blockers(self) -> None: plt.style.use("ggplot") for vb in self.map_data.vision_blockers: - if self.inside_p(point=vb): + if self.is_inside_point(point=vb): plt.text(vb[0], vb[1], "X", c="r") def _plot_minerals(self) -> None: @@ -114,7 +100,7 @@ def _plot_minerals(self) -> None: plt.style.use("ggplot") for mineral_field in self.map_data.mineral_fields: - if self.inside_p(mineral_field.position.rounded): + if self.is_inside_point(mineral_field.position.rounded): plt.scatter( mineral_field.position[0], mineral_field.position[1], color="blue" ) @@ -127,7 +113,7 @@ def _plot_geysers(self) -> None: plt.style.use("ggplot") for gasgeyser in self.map_data.normal_geysers: - if self.inside_p(gasgeyser.position.rounded): + if self.is_inside_point(gasgeyser.position.rounded): plt.scatter( gasgeyser.position[0], gasgeyser.position[1], @@ -157,20 +143,6 @@ def plot(self, self_only: bool = True, testing: bool = False) -> None: else: # pragma: no cover self.plot_perimeter(self_only=False) - @lru_cache(100) - def inside_p(self, point: Union[Point2, tuple]) -> bool: - """ - inside_p - """ - return self.polygon.is_inside_point(point) - - @lru_cache(100) - def inside_i(self, point: Union[Point2, tuple]) -> bool: # pragma: no cover - """ - inside_i - """ - return self.polygon.is_inside_indices(point) - @property def base_locations(self) -> List[Point2]: """ @@ -178,34 +150,12 @@ def base_locations(self) -> List[Point2]: """ return self.bases - # @property - # def is_reachable(self, areas): # pragma: no cover - # """ - # is connected to another areas directly - # :param areas: - # :type areas: - # :return: - # :rtype: - # """ - # pass - - @property - def get_reachable_regions(self): - """ - """ - result = [] - for r in self.region_ramps: - for reg in r.regions: - if reg != self: - result.append((str(r), reg)) - return set(result) - @property def get_area(self) -> int: """ get_area """ - return self.polygon.area + return self.area def __repr__(self) -> str: # pragma: no cover """ diff --git a/MapAnalyzer/constants.py b/MapAnalyzer/constants.py index d3d60e74..e24b025c 100644 --- a/MapAnalyzer/constants.py +++ b/MapAnalyzer/constants.py @@ -3,6 +3,7 @@ BINARY_STRUCTURE = 2 RESOURCE_BLOCKER_RADIUS = 2 NONPATHABLE_RADIUS = 0.8 +LOG_MODULE = "MapAnalyzer" COLORS = { 0 : "azure", 1 : 'black', @@ -30,4 +31,4 @@ "{level: <8}|{name: ^15}|" \ "{function: ^15}|" \ "{line: >4}|" \ - "{level.icon}{message}" + " {level.icon} {message}" diff --git a/MapAnalyzer/constructs.py b/MapAnalyzer/constructs.py index a4459c80..d1ea5cc2 100644 --- a/MapAnalyzer/constructs.py +++ b/MapAnalyzer/constructs.py @@ -41,18 +41,6 @@ def __init__( self.md_pl_choke = pathlibchoke self.is_choke = True - def calc_areas(self) -> None: - if self.main_line: - points = [min(self.points), max(self.points)] - areas = self.areas - for point in points: - point = int(point[0]), int(point[1]) - new_areas = self.map_data.where_all(point) - if self in new_areas: - new_areas.pop(new_areas.index(self)) - areas.extend(new_areas) - self.areas = list(set(areas)) - def __repr__(self) -> str: # pragma: no cover return f"<[{self.id}]ChokeArea[size={self.area}]> of {self.areas}" @@ -67,8 +55,34 @@ def __init__(self, map_data: "MapData", array: np.ndarray, ramp: sc2Ramp) -> Non self.is_ramp = True self.ramp = ramp - def calc_areas(self) -> None: - return + def closest_region(self, region_list): + return min(region_list, + key=lambda area: min(self.map_data.distance(area.center, point) for point in self.perimeter_points)) + + def set_regions(self): + from MapAnalyzer.Region import Region + for p in self.perimeter_points: + areas = self.map_data.where_all(p) + for area in areas: + if isinstance(area, VisionBlockerArea): + for sub_area in area.areas: + if isinstance(sub_area, Region) and sub_area not in self.areas: + self.areas.append(sub_area) + if isinstance(sub_area, Region) and self not in sub_area.areas: + sub_area.areas.append(self) + if isinstance(area, Region) and area not in self.areas: + self.areas.append(area) + # add ourselves to the Region Area's + if isinstance(area, Region) and self not in area.areas: + area.areas.append(self) + if len(self.regions) < 2: + # destructables blocking the ramp ? + # mineral walls ? + region_list = list(self.map_data.regions.values()) + region_list.remove(self.regions[0]) + closest_region = self.closest_region(region_list=region_list) + assert (closest_region not in self.regions) + self.areas.append(closest_region) @property def top_center(self) -> Point2: @@ -88,7 +102,7 @@ def bottom_center(self) -> Point2: self.map_data.logger.debug(f"No bottom_center found for {self}, falling back to `center`") return self.center - def __repr__(self): # pragma: no cover + def __repr__(self) -> str: # pragma: no cover return f"" def __str__(self): diff --git a/package-lock.json b/package-lock.json index 5b659a04..b66c5806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Sc2MapAnalyzer", - "version": "0.0.51", + "version": "0.0.52", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7509067e..a5a81381 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ { "name": "sc2mapanalyzer", - "version": "0.0.51" + "version": "0.0.52" } diff --git a/setup.py b/setup.py index 776265b1..8ab26cff 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ ] setup( # pragma: no cover name="sc2mapanalyzer", - version="0.0.51", + version="0.0.52", install_requires=requirements, setup_requires=["wheel", "numpy"], extras_require={ diff --git a/tests/test_suite.py b/tests/test_suite.py index 56d26247..b30cd18c 100644 --- a/tests/test_suite.py +++ b/tests/test_suite.py @@ -71,7 +71,7 @@ def test_polygon(self, map_data: MapData) -> None: def test_regions(self, map_data: MapData) -> None: for region in map_data.regions.values(): - for p in region.polygon.points: + for p in region.points: assert isinstance( map_data.where(p), Region # using where because the first return will be always Region ), f" None: assert (region in map_data.where_all(region.center)) assert (region == map_data.where(region.center)) - assert (region.corners is region.polygon.corner_points) # coverage region.plot(testing=True) + def test_ramps(self, map_data: MapData) -> None: + for ramp in map_data.map_ramps: + assert (len(ramp.regions) == 2), f"ramp = {ramp}" + def test_chokes(self, map_data: MapData) -> None: for choke in map_data.map_chokes: for p in choke.points: