-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
C pathfinding and chokes #111
Changes from 36 commits
5f067b7
6e9c117
2481078
86ddc3f
21d0e6f
4a36e39
5223e31
d02dedc
f444ef6
6b80751
4e18c0c
e601afb
aeee8b4
1623709
a14871b
1fa3af7
aa2d597
6288c07
4a1dec2
5e6e730
5159b8e
bf9aebb
4334dde
b26f4e1
7e1cc78
5207496
a8ad54a
5341e5a
334830e
cf93010
126915e
cd7849c
7839676
fc7c184
4750130
787c6b8
c665246
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,13 +13,14 @@ | |
from MapAnalyzer.Debugger import MapAnalyzerDebugger | ||
from MapAnalyzer.Pather import MapAnalyzerPather | ||
from MapAnalyzer.Region import Region | ||
from MapAnalyzer.utils import get_sets_with_mutual_elements | ||
from MapAnalyzer.utils import get_sets_with_mutual_elements, fix_map_ramps | ||
|
||
from .constants import BINARY_STRUCTURE, CORNER_MIN_DISTANCE, MAX_REGION_AREA, MIN_REGION_AREA | ||
|
||
from .decorators import progress_wrapped | ||
from .exceptions import CustomDeprecationWarning | ||
from MapAnalyzer.constructs import ChokeArea, MDRamp, VisionBlockerArea, PathLibChoke | ||
from MapAnalyzer.constructs import ChokeArea, MDRamp, VisionBlockerArea, RawChoke | ||
from .cext import CMapInfo, CMapChoke | ||
|
||
try: | ||
__version__ = get_distribution('sc2mapanalyzer') | ||
|
@@ -40,6 +41,11 @@ def __init__(self, bot: BotAI, loglevel: str = "ERROR", arcade: bool = False, | |
corner_distance: int = CORNER_MIN_DISTANCE) -> None: | ||
# store relevant data from api | ||
self.bot = bot | ||
# temporary fix to set ramps correctly if they are broken in burnysc2 due to having | ||
# destructables on them. ramp sides don't consider the destructables now, | ||
# should update them during the game | ||
self.bot.game_info.map_ramps, self.bot.game_info.vision_blockers = fix_map_ramps(self.bot) | ||
|
||
self.corner_distance = corner_distance # the lower this value is, the sharper the corners will be | ||
self.arcade = arcade | ||
self.version = __version__ | ||
|
@@ -63,17 +69,21 @@ def __init__(self, bot: BotAI, loglevel: str = "ERROR", arcade: bool = False, | |
self.map_vision_blockers: list = [] # set later on compile | ||
self.vision_blockers_labels: list = [] # set later on compile | ||
self.vision_blockers_grid: list = [] # set later on compile | ||
self.overlord_spots: list = [] | ||
self.resource_blockers = [Point2((m.position[0], m.position[1])) for m in self.bot.all_units if | ||
any(x in m.name.lower() for x in {"rich", "450"})] | ||
self.pathlib_to_local_chokes = None | ||
self.overlapping_choke_ids = None | ||
|
||
pathing_grid = np.fmax(self.path_arr, self.placement_arr) | ||
self.c_ext_map = CMapInfo(pathing_grid.T, self.terrain_height.T, self.bot.game_info.playable_area) | ||
self.overlord_spots = self.c_ext_map.overlord_spots | ||
|
||
# plugins | ||
self.log_level = loglevel | ||
self.debugger = MapAnalyzerDebugger(self, loglevel=self.log_level) | ||
self.pather = MapAnalyzerPather(self) | ||
|
||
self.connectivity_graph = None # set by pather | ||
self.pathlib_map = self.pather.pathlib_map | ||
self.pyastar = self.pather.pyastar | ||
self.nonpathable_indices_stacked = self.pather.nonpathable_indices_stacked | ||
|
||
|
@@ -192,7 +202,7 @@ def find_lowest_cost_points(self, from_pos: Point2, radius: float, grid: np.ndar | |
def get_climber_grid(self, default_weight: float = 1, include_destructables: bool = True) -> ndarray: | ||
""" | ||
:rtype: numpy.ndarray | ||
Climber grid is a grid modified by :mod:`sc2pathlibp`, and is used for units that can climb, | ||
Climber grid is a grid modified by the c extension, and is used for units that can climb, | ||
|
||
such as Reaper, Colossus | ||
|
||
|
@@ -302,6 +312,12 @@ def pathfind(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[ | |
return self.pather.pathfind(start=start, goal=goal, grid=grid, allow_diagonal=allow_diagonal, | ||
sensitivity=sensitivity) | ||
|
||
def pathfind_c(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[float, float], Point2], | ||
grid: Optional[ndarray] = None, smoothing: bool = False, | ||
sensitivity: int = 1) -> Optional[List[Point2]]: | ||
return self.pather.pathfind_c(start=start, goal=goal, grid=grid, smoothing=smoothing, | ||
sensitivity=sensitivity) | ||
|
||
def add_cost(self, position: Tuple[float, float], radius: float, grid: ndarray, weight: float = 100, safe: bool = True, | ||
initial_default_weights: float = 0) -> ndarray: | ||
""" | ||
|
@@ -600,17 +616,13 @@ def in_region_p(self, point: Union[Point2, tuple]) -> Optional[Region]: | |
|
||
def _clean_plib_chokes(self) -> None: | ||
# needs to be called AFTER MDramp and VisionBlocker are populated | ||
raw_chokes = self.pathlib_map.chokes | ||
self.pathlib_to_local_chokes = [] | ||
for i, c in enumerate(raw_chokes): | ||
self.pathlib_to_local_chokes.append(PathLibChoke(pathlib_choke=c, pk=i)) | ||
areas = self.map_ramps.copy() | ||
areas.extend(self.map_vision_blockers) | ||
self.overlapping_choke_ids = self._get_overlapping_chokes(local_chokes=self.pathlib_to_local_chokes, | ||
self.overlapping_choke_ids = self._get_overlapping_chokes(local_chokes=self.c_ext_map.chokes, | ||
areas=areas) | ||
|
||
@staticmethod | ||
def _get_overlapping_chokes(local_chokes: List[PathLibChoke], | ||
def _get_overlapping_chokes(local_chokes: List[CMapChoke], | ||
areas: Union[List[MDRamp], List[Union[MDRamp, VisionBlockerArea]]]) -> Set[int]: | ||
li = [] | ||
for area in areas: | ||
|
@@ -684,10 +696,15 @@ def _calc_grid(self) -> None: | |
self.vision_blockers_labels = np.unique(vb_labeled_array) | ||
|
||
def _set_map_ramps(self): | ||
# some ramps coming from burnysc2 have broken data and the bottom_center and top_center | ||
# may even be the same. by removing them they should be tagged as chokes in the c extension | ||
# if they really are ones | ||
viable_ramps = list(filter(lambda x: x.bottom_center.distance_to(x.top_center) >= 1, | ||
self.bot.game_info.map_ramps)) | ||
self.map_ramps = [MDRamp(map_data=self, | ||
ramp=r, | ||
array=self.points_to_numpy_array(r.points)) | ||
for r in self.bot.game_info.map_ramps] | ||
for r in viable_ramps] | ||
|
||
def _calc_vision_blockers(self) -> None: | ||
# compute VisionBlockerArea | ||
|
@@ -713,7 +730,7 @@ def _calc_chokes(self) -> None: | |
# compute ChokeArea | ||
|
||
self._clean_plib_chokes() | ||
chokes = [c for c in self.pathlib_to_local_chokes if c.id not in self.overlapping_choke_ids] | ||
chokes = [c for c in self.c_ext_map.chokes if c.id not in self.overlapping_choke_ids] | ||
self.map_chokes = self.map_ramps.copy() | ||
self.map_chokes.extend(self.map_vision_blockers) | ||
|
||
|
@@ -726,7 +743,7 @@ def _calc_chokes(self) -> None: | |
cm = int(cm[0]), int(cm[1]) | ||
areas = self.where_all(cm) | ||
|
||
new_choke = ChokeArea( | ||
new_choke = RawChoke( | ||
map_data=self, array=new_choke_array, pathlibchoke=choke | ||
) | ||
for area in areas: | ||
|
@@ -835,6 +852,26 @@ def plot_influenced_path(self, | |
fontdict=fontdict, | ||
allow_diagonal=allow_diagonal) | ||
|
||
def plot_influenced_path_c(self, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, the C method is going to be the Supported method by the lib, there is no reason to give it a specific name, any other method that might join , should have a specific name to differentiate it |
||
start: Union[Tuple[float, float], Point2], | ||
goal: Union[Tuple[float, float], Point2], | ||
weight_array: ndarray, | ||
smoothing: bool = False, | ||
name: Optional[str] = None, | ||
fontdict: dict = None) -> None: | ||
""" | ||
|
||
A useful debug utility method for experimenting with the :mod:`.Pather` module | ||
|
||
""" | ||
|
||
self.debugger.plot_influenced_path_c(start=start, | ||
goal=goal, | ||
weight_array=weight_array, | ||
smoothing=smoothing, | ||
name=name, | ||
fontdict=fontdict) | ||
|
||
def _plot_regions(self, fontdict: Dict[str, Union[str, int]]) -> None: | ||
return self.debugger.plot_regions(fontdict=fontdict) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
import numpy as np | ||
import pyastar.astar_wrapper as pyastar | ||
|
||
from loguru import logger | ||
from numpy import ndarray | ||
from sc2.ids.unit_typeid import UnitTypeId as UnitID | ||
|
@@ -11,7 +12,7 @@ | |
from MapAnalyzer.constants import GEYSER_RADIUS_FACTOR, NONPATHABLE_RADIUS_FACTOR, RESOURCE_BLOCKER_RADIUS_FACTOR | ||
from MapAnalyzer.exceptions import OutOfBoundsException, PatherNoPointsException | ||
from MapAnalyzer.Region import Region | ||
from .sc2pathlibp import Sc2Map | ||
from .cext import astar_path | ||
|
||
if TYPE_CHECKING: | ||
from MapAnalyzer.MapData import MapData | ||
|
@@ -23,13 +24,7 @@ class MapAnalyzerPather: | |
def __init__(self, map_data: "MapData") -> None: | ||
self.map_data = map_data | ||
self.pyastar = pyastar | ||
self.pathlib_map = None | ||
self._set_pathlib_map() | ||
if self.pathlib_map: | ||
# noinspection PyProtectedMember | ||
self._climber_grid = np.array(self.pathlib_map._map.reaper_pathing).astype(np.float32) | ||
else: | ||
logger.error('Could not set Pathlib Map') | ||
|
||
nonpathable_indices = np.where(self.map_data.bot.game_info.pathing_grid.data_numpy == 0) | ||
self.nonpathable_indices_stacked = np.column_stack( | ||
(nonpathable_indices[1], nonpathable_indices[0]) | ||
|
@@ -63,17 +58,6 @@ def find_all_paths(self, start: Region, goal: Region, path: Optional[List[Region | |
paths.append(newpath) | ||
return paths | ||
|
||
def _set_pathlib_map(self) -> None: | ||
""" | ||
Will initialize the sc2pathlib `SC2Map` object for future use | ||
""" | ||
self.pathlib_map = Sc2Map( | ||
self.map_data.path_arr, | ||
self.map_data.placement_arr, | ||
self.map_data.terrain_height, | ||
self.map_data.bot.game_info.playable_area, | ||
) | ||
|
||
def _add_non_pathables_ground(self, grid: ndarray, include_destructables: bool = True) -> ndarray: | ||
nonpathables = self.map_data.bot.structures.not_flying | ||
nonpathables.extend(self.map_data.bot.enemy_structures.not_flying) | ||
|
@@ -130,9 +114,11 @@ def get_base_pathing_grid(self) -> ndarray: | |
|
||
def get_climber_grid(self, default_weight: float = 1, include_destructables: bool = True) -> ndarray: | ||
"""Grid for units like reaper / colossus """ | ||
grid = self._climber_grid.copy() | ||
grid = self.get_base_pathing_grid() | ||
grid = np.where(grid != 0, default_weight, np.inf).astype(np.float32) | ||
grid = np.where(self.map_data.c_ext_map.climber_grid != 0, default_weight, grid).astype(np.float32) | ||
grid = self._add_non_pathables_ground(grid=grid, include_destructables=include_destructables) | ||
|
||
return grid | ||
|
||
def get_clean_air_grid(self, default_weight: float = 1) -> ndarray: | ||
|
@@ -148,7 +134,7 @@ def get_air_vs_ground_grid(self, default_weight: float) -> ndarray: | |
return air_vs_ground_grid | ||
|
||
def get_pyastar_grid(self, default_weight: float = 1, include_destructables: bool = True) -> ndarray: | ||
grid = self.map_data.pather.get_base_pathing_grid() | ||
grid = self.get_base_pathing_grid() | ||
grid = np.where(grid != 0, default_weight, np.inf).astype(np.float32) | ||
grid = self._add_non_pathables_ground(grid=grid, include_destructables=include_destructables) | ||
return grid | ||
|
@@ -184,6 +170,39 @@ def pathfind(self, start: Tuple[float, float], goal: Tuple[float, float], grid: | |
logger.debug(f"No Path found s{start}, g{goal}") | ||
return None | ||
|
||
def pathfind_c(self, start: Tuple[float, float], goal: Tuple[float, float], grid: Optional[ndarray] = None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just marking the places for convenience |
||
smoothing: bool = False, | ||
sensitivity: int = 1) -> ndarray: | ||
if start is not None and goal is not None: | ||
start = int(round(start[0])), int(round(start[1])) | ||
goal = int(round(goal[0])), int(round(goal[1])) | ||
else: | ||
logger.warning(PatherNoPointsException(start=start, goal=goal)) | ||
return None | ||
if grid is None: | ||
logger.warning("Using the default pyastar grid as no grid was provided.") | ||
grid = self.get_pyastar_grid() | ||
|
||
path = astar_path(grid, start, goal, smoothing) | ||
|
||
if path is not None: | ||
path = list(map(Point2, path))[::sensitivity] | ||
""" | ||
Edge case | ||
EverDreamLE, (81, 29) is considered in map bounds, but it is not. | ||
""" | ||
# `if point` is checking with burnysc2 that the point is in map bounds | ||
if 'everdream' in self.map_data.map_name.lower(): | ||
legal_path = [point for point in path if point and point.x != 81 and point.y != 29] | ||
else: # normal case | ||
legal_path = [point for point in path if point] | ||
|
||
legal_path.pop(0) | ||
return legal_path | ||
else: | ||
logger.debug(f"No Path found s{start}, g{goal}") | ||
return None | ||
|
||
@staticmethod | ||
def add_cost(position: Tuple[float, float], radius: float, arr=ndarray, weight: float = 100, | ||
safe: bool = True, initial_default_weights: float = 0) -> ndarray: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .wrapper import astar_path, CMapInfo, CMapChoke |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably the default method for plotting from now and on,
i would rename the old function to have a pyastar postfix. especially since its life span is is not much longer in this project