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: