Skip to content

Commit

Permalink
Merge pull request #149 from eladyaniv01/develop
Browse files Browse the repository at this point in the history
Release 0.0.85

* Removed pyastar dependency and the deprecated pathfind_pyastar function completely  ([c310ac4](c310ac4))
* Added support for using nyduses with pathfinding ([636af5e](636af5e))
  • Loading branch information
eladyaniv01 authored Mar 27, 2021
2 parents 4120ae8 + fc52f6b commit 2fb0784
Show file tree
Hide file tree
Showing 14 changed files with 720 additions and 127 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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.85](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.84...v0.0.85) (2021-03-27)

* Removed pyastar dependency and the deprecated pathfind_pyastar function completely ([c310ac4c](https://github.com/eladyaniv01/SC2MapAnalysis/commit/c310ac4cfca7e85d499da33822060c567993e145))
* Added support for using nyduses with pathfinding ([636af5ea](https://github.com/eladyaniv01/SC2MapAnalysis/commit/636af5ea8bd233d27e0a5a68699c4390de250614))

### [0.0.84](https://github.com/eladyaniv01/SC2MapAnalysis/compare/v0.0.83...v0.0.84) (2021-02-03)

### Refactoring
Expand Down
33 changes: 18 additions & 15 deletions MapAnalyzer/Debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,13 @@ def plot_influenced_path(self, start: Union[Tuple[float, float], Point2],
plt.title(f"{name}", fontdict=fontdict, loc='right')
plt.grid()

def plot_influenced_path_pyastar(self, start: Union[Tuple[int, int], Point2],
goal: Union[Tuple[int, int], Point2],
weight_array: ndarray,
allow_diagonal=False,
name: Optional[str] = None,
fontdict: dict = None) -> None:
def plot_influenced_path_nydus(self, start: Union[Tuple[float, float], Point2],
goal: Union[Tuple[float, float], Point2],
weight_array: ndarray,
large: bool = False,
smoothing: bool = False,
name: Optional[str] = None,
fontdict: dict = None) -> None:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.cm import ScalarMappable
Expand All @@ -282,16 +283,18 @@ def plot_influenced_path_pyastar(self, start: Union[Tuple[int, int], Point2],
if name is None:
name = self.map_data.map_name
arr = weight_array.copy()
path = self.map_data.pathfind_pyastar(start, goal,
grid=arr,
sensitivity=1,
allow_diagonal=allow_diagonal)
paths = self.map_data.pathfind_with_nyduses(start, goal,
grid=arr,
large=large,
smoothing=smoothing,
sensitivity=1)
ax: plt.Axes = plt.subplot(1, 1, 1)
if path is not None:
path = np.flipud(path) # for plot align
logger.info("Found")
x, y = zip(*path)
ax.scatter(x, y, s=3, c='green')
if paths is not None:
for i in range(len(paths[0])):
path = np.flipud(paths[0][i]) # for plot align
logger.info("Found")
x, y = zip(*path)
ax.scatter(x, y, s=3, c='green')
else:
logger.info("Not Found")

Expand Down
94 changes: 48 additions & 46 deletions MapAnalyzer/MapData.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def __init__(self, bot: BotAI, loglevel: str = "ERROR", arcade: bool = False,
self.pather = MapAnalyzerPather(self)

self.connectivity_graph = None # set by pather
self.pyastar = self.pather.pyastar
self.nonpathable_indices_stacked = self.pather.nonpathable_indices_stacked

# compile
Expand Down Expand Up @@ -267,10 +266,9 @@ def get_clean_air_grid(self, default_weight: float = 1) -> ndarray:
"""
return self.pather.get_clean_air_grid(default_weight=default_weight)

def pathfind_pyastar(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[float, float], Point2],
grid: Optional[ndarray] = None,
allow_diagonal: bool = False, sensitivity: int = 1) -> Optional[List[Point2]]:

def pathfind(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[float, float], Point2],
grid: Optional[ndarray] = None, large: bool = False, smoothing: bool = False,
sensitivity: int = 1) -> Optional[List[Point2]]:
"""
:rtype: Union[List[:class:`sc2.position.Point2`], None]
Will return the path with lowest cost (sum) given a weighted array (``grid``), ``start`` , and ``goal``.
Expand All @@ -280,48 +278,51 @@ def pathfind_pyastar(self, start: Union[Tuple[float, float], Point2], goal: Unio
If no path is possible, will return ``None``
Tip:
``sensitivity`` indicates how to slice the path,
just like doing: ``result_path = path[::sensitivity]``
where ``path`` is the return value from this function
this is useful since in most use cases you wouldn't want
to get each and every single point,
getting every n-``th`` point works better in practice
``sensitivity`` indicates how to slice the path,
just like doing: ``result_path = path[::sensitivity]``
where ``path`` is the return value from this function
Caution:
``allow_diagonal=True`` will result in a slight performance penalty.
this is useful since in most use cases you wouldn't want
to get each and every single point,
`However`, if you don't over-use it, it will naturally generate shorter paths,
getting every n-``th`` point works better in practice
by converting(for example) ``move_right + move_up`` into ``move_top_right`` etc.
`` large`` is a boolean that determines whether we are doing pathing with large unit sizes
like Thor and Ultralisk. When it's false the pathfinding is using unit size 1, so if
you want to a guarantee that a unit with size > 1 fits through the path then large should be True.
TODO:
more examples for different usages available
``smoothing`` tries to do a similar thing on the c side but to the maximum extent possible.
it will skip all the waypoints it can if taking the straight line forward is better
according to the influence grid
Example:
>>> my_grid = self.get_pyastar_grid()
>>> # start / goal could be any tuple / Point2
>>> st, gl = (50,75) , (100,100)
>>> path = self.pathfind_pyastar(start=st,goal=gl,grid=my_grid,allow_diagonal=True, sensitivity=3)
>>> path = self.pathfind(start=st,goal=gl,grid=my_grid, large=False, smoothing=False, sensitivity=3)
See Also:
* :meth:`.MapData.get_pyastar_grid`
* :meth:`.MapData.find_lowest_cost_points`
"""
return self.pather.pathfind_pyastar(start=start, goal=goal, grid=grid, allow_diagonal=allow_diagonal,
sensitivity=sensitivity)
return self.pather.pathfind(start=start, goal=goal, grid=grid, large=large, smoothing=smoothing,
sensitivity=sensitivity)

def pathfind(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[float, float], Point2],
def pathfind_with_nyduses(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[float, float], Point2],
grid: Optional[ndarray] = None, large: bool = False, smoothing: bool = False,
sensitivity: int = 1) -> Optional[List[Point2]]:
sensitivity: int = 1) -> Optional[Tuple[List[List[Point2]], Optional[List[int]]]]:
"""
:rtype: Union[List[:class:`sc2.position.Point2`], None]
:rtype: Union[List[List[:class:`sc2.position.Point2`]], None]
Will return the path with lowest cost (sum) given a weighted array (``grid``), ``start`` , and ``goal``.
Returns a tuple where the first part is a list of path segments, second part is list of 2 tags for the
nydus network units that were used.
If one path segment is returned, it is a path from start node to goal node, no nydus node was used and
the second part of the tuple is None.
If two path segments are returned, the first one is from start node to a nydus network entrance,
and the second one is from some other nydus network entrance to the goal node. The second part of the tuple
includes first the tag of the nydus network node you should go into, and then the tag of the node you come
out from.
**IF NO** ``grid`` **has been provided**, will request a fresh grid from :class:`.Pather`
Expand Down Expand Up @@ -355,8 +356,8 @@ def pathfind(self, start: Union[Tuple[float, float], Point2], goal: Union[Tuple[
* :meth:`.MapData.find_lowest_cost_points`
"""
return self.pather.pathfind(start=start, goal=goal, grid=grid, large=large, smoothing=smoothing,
sensitivity=sensitivity)
return self.pather.pathfind_with_nyduses(start=start, goal=goal, grid=grid, large=large, smoothing=smoothing,
sensitivity=sensitivity)

def add_cost(self, position: Tuple[float, float], radius: float, grid: ndarray, weight: float = 100,
safe: bool = True,
Expand Down Expand Up @@ -890,28 +891,29 @@ def plot_map(
logger.error(f"{inspect.stack()[1]}")
self.debugger.plot_map(fontdict=fontdict, figsize=figsize)

def plot_influenced_path_pyastar(self,

start: Union[Tuple[float, float], Point2],
goal: Union[Tuple[float, float], Point2],
weight_array: ndarray,
allow_diagonal=False,
name: Optional[str] = None,
fontdict: dict = None) -> None:
def plot_influenced_path(self,
start: Union[Tuple[float, float], Point2],
goal: Union[Tuple[float, float], Point2],
weight_array: ndarray,
large: bool = False,
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_pyastar(start=start,
goal=goal,
weight_array=weight_array,
name=name,
fontdict=fontdict,
allow_diagonal=allow_diagonal)
self.debugger.plot_influenced_path(start=start,
goal=goal,
weight_array=weight_array,
large=large,
smoothing=smoothing,
name=name,
fontdict=fontdict)

def plot_influenced_path(self,
def plot_influenced_path_nydus(self,
start: Union[Tuple[float, float], Point2],
goal: Union[Tuple[float, float], Point2],
weight_array: ndarray,
Expand All @@ -925,7 +927,7 @@ def plot_influenced_path(self,
"""

self.debugger.plot_influenced_path(start=start,
self.debugger.plot_influenced_path_nydus(start=start,
goal=goal,
weight_array=weight_array,
large=large,
Expand Down
80 changes: 59 additions & 21 deletions MapAnalyzer/Pather.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import List, Optional, Tuple, TYPE_CHECKING

import numpy as np
import pyastar.astar_wrapper as pyastar

from loguru import logger
from numpy import ndarray
Expand All @@ -11,7 +10,7 @@
from MapAnalyzer.exceptions import OutOfBoundsException, PatherNoPointsException
from MapAnalyzer.Region import Region
from MapAnalyzer.utils import change_destructable_status_in_grid
from .cext import astar_path
from .cext import astar_path, astar_path_with_nyduses
from .destructibles import *

if TYPE_CHECKING:
Expand Down Expand Up @@ -46,7 +45,6 @@ class MapAnalyzerPather:

def __init__(self, map_data: "MapData") -> None:
self.map_data = map_data
self.pyastar = pyastar

nonpathable_indices = np.where(self.map_data.bot.game_info.pathing_grid.data_numpy == 0)
self.nonpathable_indices_stacked = np.column_stack(
Expand Down Expand Up @@ -294,9 +292,10 @@ def get_pyastar_grid(self, default_weight: float = 1, include_destructables: boo
grid = np.where(grid != 0, default_weight, np.inf).astype(np.float32)
return grid

def pathfind_pyastar(self, start: Tuple[float, float], goal: Tuple[float, float], grid: Optional[ndarray] = None,
allow_diagonal: bool = False, sensitivity: int = 1) -> Optional[List[Point2]]:

def pathfind(self, start: Tuple[float, float], goal: Tuple[float, float], grid: Optional[ndarray] = None,
large: bool = False,
smoothing: bool = False,
sensitivity: int = 1) -> Optional[List[Point2]]:
if grid is None:
logger.warning("Using the default pyastar grid as no grid was provided.")
grid = self.get_pyastar_grid()
Expand All @@ -314,20 +313,29 @@ def pathfind_pyastar(self, start: Tuple[float, float], goal: Tuple[float, float]
if start is None or goal is None:
return None

path = self.pyastar.astar_path(grid, start=start, goal=goal, allow_diagonal=allow_diagonal)
path = astar_path(grid, start, goal, large, smoothing)

if path is not None:
path = list(map(Point2, path))[::sensitivity]
# Remove the starting point from the path.
# Make sure the goal node is the last node even if we are
# skipping points
complete_path = list(map(Point2, path))
skipped_path = complete_path[0:-1:sensitivity]
if skipped_path:
skipped_path.pop(0)

path.pop(0)
return path
skipped_path.append(complete_path[-1])

return skipped_path
else:
logger.debug(f"No Path found s{start}, g{goal}")
return None

def pathfind(self, start: Tuple[float, float], goal: Tuple[float, float], grid: Optional[ndarray] = None,
large: bool = False,
smoothing: bool = False,
sensitivity: int = 1) -> Optional[List[Point2]]:
def pathfind_with_nyduses(self, start: Tuple[float, float], goal: Tuple[float, float],
grid: Optional[ndarray] = None,
large: bool = False,
smoothing: bool = False,
sensitivity: int = 1) -> Optional[Tuple[List[List[Point2]], Optional[List[int]]]]:
if grid is None:
logger.warning("Using the default pyastar grid as no grid was provided.")
grid = self.get_pyastar_grid()
Expand All @@ -345,13 +353,43 @@ def pathfind(self, start: Tuple[float, float], goal: Tuple[float, float], grid:
if start is None or goal is None:
return None

path = astar_path(grid, start, goal, large, smoothing)

if path is not None:
path = list(map(Point2, path))[::sensitivity]
path.pop(0)

return path
nydus_units = self.map_data.bot.structures.of_type([UnitTypeId.NYDUSNETWORK, UnitTypeId.NYDUSCANAL]).ready
nydus_positions = [nydus.position for nydus in nydus_units]

paths = astar_path_with_nyduses(grid, start, goal,
nydus_positions,
large, smoothing)
if paths is not None:
returned_path = []
nydus_tags = None
if len(paths) == 1:
path = list(map(Point2, paths[0]))
skipped_path = path[0:-1:sensitivity]
if skipped_path:
skipped_path.pop(0)
skipped_path.append(path[-1])
returned_path.append(skipped_path)
else:
first_path = list(map(Point2, paths[0]))
first_skipped_path = first_path[0:-1:sensitivity]
if first_skipped_path:
first_skipped_path.pop(0)
first_skipped_path.append(first_path[-1])
returned_path.append(first_skipped_path)

enter_nydus_unit = nydus_units.filter(lambda x: x.position.rounded == first_path[-1]).first
enter_nydus_tag = enter_nydus_unit.tag

second_path = list(map(Point2, paths[1]))
exit_nydus_unit = nydus_units.filter(lambda x: x.position.rounded == second_path[0]).first
exit_nydus_tag = exit_nydus_unit.tag
nydus_tags = [enter_nydus_tag, exit_nydus_tag]

second_skipped_path = second_path[0:-1:sensitivity]
second_skipped_path.append(second_path[-1])
returned_path.append(second_skipped_path)

return returned_path, nydus_tags
else:
logger.debug(f"No Path found s{start}, g{goal}")
return None
Expand Down
2 changes: 1 addition & 1 deletion MapAnalyzer/cext/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .wrapper import astar_path, CMapInfo, CMapChoke
from .wrapper import astar_path, astar_path_with_nyduses, CMapInfo, CMapChoke
Binary file modified MapAnalyzer/cext/mapanalyzerext.so
Binary file not shown.
Loading

0 comments on commit 2fb0784

Please sign in to comment.