Source code for MapAnalyzer.Polygon

from functools import lru_cache
from typing import List, Set, Tuple, TYPE_CHECKING, Union

import numpy as np
from loguru import logger
from numpy import int64, ndarray
from sc2.position import Point2
from scipy.ndimage import center_of_mass

if TYPE_CHECKING:
    from MapAnalyzer import MapData, Region


[docs]class Buildables: """ Represents the Buildable Points in a :class:`.Polygon`, "Lazy" class that will only update information when it is needed Tip: :class:`.BuildablePoints` that belong to a :class:`.ChokeArea` are always the edges, this is useful for walling off """ def __init__(self, polygon): self.polygon = polygon self.points = None @property
[docs] def free_pct(self) -> float: """ A simple method for knowing what % of the points is left available out of the total """ if self.points is None: logger.warning("BuildablePoints needs to update first") self.update() return len(self.points) / len(self.polygon.points)
[docs] def update(self) -> None: """ To be called only by :class:`.Polygon`, this ensures that updates are done in a lazy fashion, the update is evaluated only when there is need for the information, otherwise it is ignored """ parr = self.polygon.map_data.points_to_numpy_array(self.polygon.points) # passing safe false to reduce the warnings, # which are irrelevant in this case [self.polygon.map_data.add_cost(position=(unit.position.x, unit.position.y), radius=unit.radius * 0.9, grid=parr, safe=False) for unit in self.polygon.map_data.bot.all_units.not_flying] buildable_indices = np.where(parr == 1) buildable_points = [] _points = list(self.polygon.map_data.indices_to_points(buildable_indices)) placement_grid = self.polygon.map_data.placement_arr.T for p in _points: if p[0] < placement_grid.shape[0] and p[1] < placement_grid.shape[1]: if placement_grid[p] == 1: buildable_points.append(p) self.points = list(map(Point2, buildable_points))
[docs]class Polygon: """ Base Class for Representing an "Area" """ # noinspection PyProtectedMember def __init__(self, map_data: "MapData", array: ndarray) -> None: # pragma: no cover self.map_data = map_data self.array = array self.indices = np.where(self.array == 1) self._clean_points = self.map_data.indices_to_points(self.indices) self.points = set([Point2(p) for p in self._clean_points]) # this is to serve data for map data compile, the accurate # calculation will be done on _set_points self._set_points() self.id = None # TODO self.is_choke = False self.is_ramp = False self.is_vision_blocker = False self.is_region = False self.areas = [] # set by map_data / Region self.map_data.polygons.append(self) self._buildables = Buildables(polygon=self) @property def top(self): return max(self.points, key=lambda x: (x[1], 0)) @property def bottom(self): return min(self.points, key=lambda x: (x[1], 0)) @property def right(self): return max(self.points, key=lambda x: (x[0], 0)) @property def left(self): return min(self.points, key=lambda x: (x[0], 0)) def _set_points(self): points = [p for p in self._clean_points] points.extend(self.corner_points) points.extend(self.perimeter_points) self.points = set([Point2((int(p[0]), int(p[1]))) for p in points]) self.indices = self.map_data.points_to_indices(self.points) @property
[docs] def buildables(self) -> Buildables: """ :rtype: :class:`.BuildablePoints` Is a responsible for holding and updating the buildable points of it's respected :class:`.Polygon` """ self._buildables.update() return self._buildables
@property
[docs] def regions(self) -> List["Region"]: """ :rtype: List[:class:`.Region`] Filters out every Polygon that is not a region, and is inside / bordering with ``self`` """ from MapAnalyzer.Region import Region if len(self.areas) > 0: return [r for r in self.areas if isinstance(r, Region)] return []
def calc_areas(self) -> None: # This is called by MapData, at a specific point in the sequence of compiling the map # this method uses where_all which means # it should be called at the end of the map compilation when areas are populated points = self.perimeter_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))
[docs] def plot(self, testing: bool = False) -> None: # pragma: no cover """ plot """ import matplotlib.pyplot as plt plt.style.use("ggplot") plt.imshow(self.array, origin="lower") if testing: return plt.show()
@property @lru_cache()
[docs] def nodes(self) -> List[Point2]: """ List of :class:`.Point2` """ return [p for p in self.points]
@property @lru_cache()
[docs] def corner_array(self) -> ndarray: """ :rtype: :class:`.ndarray` """ from skimage.feature import corner_harris, corner_peaks array = corner_peaks( corner_harris(self.array), min_distance=self.map_data.corner_distance, threshold_rel=0.01) return array
@property @lru_cache()
[docs] def width(self) -> float: """ Lazy width calculation, will be approx 0.5 < x < 1.5 of real width """ pl = list(self.perimeter_points) s1 = min(pl) s2 = max(pl) x1, y1 = s1[0], s1[1] x2, y2 = s2[0], s2[1] return np.math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
@property @lru_cache()
[docs] def corner_points(self) -> List[Point2]: """ :rtype: List[:class:`.Point2`] """ points = [Point2((int(p[0]), int(p[1]))) for p in self.corner_array if self.is_inside_point(Point2(p))] return points
@property def clean_points(self) -> List[Tuple[int64, int64]]: # For internal usage return list(self._clean_points) # needs to be array-like for numpy calculations @property
[docs] def center(self) -> Point2: """ Since the center is always going to be a ``float``, and for performance considerations we use integer coordinates. We will return the closest point registered """ cm = self.map_data.closest_towards_point(points=self.clean_points, target=center_of_mass(self.array)) return cm
@lru_cache()
[docs] def is_inside_point(self, point: Union[Point2, tuple]) -> bool: """ Query via Set(Point2) ''fast'' """ if isinstance(point, Point2): point = point.rounded if point in self.points: return True return False
@lru_cache()
[docs] def is_inside_indices( self, point: Union[Point2, tuple] ) -> bool: # pragma: no cover """ Query via 2d np.array ''slower'' """ if isinstance(point, Point2): point = point.rounded return point[0] in self.indices[0] and point[1] in self.indices[1]
@property
[docs] def perimeter(self) -> np.ndarray: """ The perimeter is interpolated between inner and outer cell-types using broadcasting """ isolated_region = self.array xx, yy = np.gradient(isolated_region) edge_indices = np.argwhere(xx ** 2 + yy ** 2 > 0.1) return edge_indices
@property
[docs] def perimeter_points(self) -> Set[Tuple[int64, int64]]: """ Useful method for getting perimeter points """ li = [Point2((int(p[0]), int(p[1]))) for p in self.perimeter] return set(li)
@property
[docs] def area(self) -> int: """ Sum of all points """ return len(self.points)
def __repr__(self) -> str: return f"<Polygon[size={self.area}]: {self.areas}>"