diff --git a/CHANGELOG.md b/CHANGELOG.md index f99db2c..4a766a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/MapAnalyzer/Debugger.py b/MapAnalyzer/Debugger.py index 8d66061..ccdad8b 100644 --- a/MapAnalyzer/Debugger.py +++ b/MapAnalyzer/Debugger.py @@ -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 @@ -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") diff --git a/MapAnalyzer/MapData.py b/MapAnalyzer/MapData.py index ef9f139..1852215 100644 --- a/MapAnalyzer/MapData.py +++ b/MapAnalyzer/MapData.py @@ -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 @@ -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``. @@ -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` @@ -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, @@ -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, @@ -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, diff --git a/MapAnalyzer/Pather.py b/MapAnalyzer/Pather.py index d37d601..8dab074 100644 --- a/MapAnalyzer/Pather.py +++ b/MapAnalyzer/Pather.py @@ -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 @@ -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: @@ -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( @@ -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() @@ -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() @@ -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 diff --git a/MapAnalyzer/cext/__init__.py b/MapAnalyzer/cext/__init__.py index af3d433..695b0d3 100644 --- a/MapAnalyzer/cext/__init__.py +++ b/MapAnalyzer/cext/__init__.py @@ -1 +1 @@ -from .wrapper import astar_path, CMapInfo, CMapChoke +from .wrapper import astar_path, astar_path_with_nyduses, CMapInfo, CMapChoke diff --git a/MapAnalyzer/cext/mapanalyzerext.so b/MapAnalyzer/cext/mapanalyzerext.so index 3136bd1..662427d 100644 Binary files a/MapAnalyzer/cext/mapanalyzerext.so and b/MapAnalyzer/cext/mapanalyzerext.so differ diff --git a/MapAnalyzer/cext/src/ma_ext.c b/MapAnalyzer/cext/src/ma_ext.c index 36b7ae3..a65d674 100644 --- a/MapAnalyzer/cext/src/ma_ext.c +++ b/MapAnalyzer/cext/src/ma_ext.c @@ -413,6 +413,7 @@ typedef struct Node { int idx; float cost; int path_length; + uint8_t is_nydus; } Node; /* @@ -742,6 +743,322 @@ static int run_pathfind(MemoryArena *arena, float *weights, int* paths, int w, i return path_length; } +static inline uint8_t is_nydus_node(int *nydus_nodes, int nydus_count, int node, int map_width) +{ + for (int i = 0; i < nydus_count; ++i) + { + int nydus_x = nydus_nodes[i] % map_width; + int nydus_y = nydus_nodes[i] / map_width; + int node_x = node % map_width; + int node_y = node / map_width; + + //Nydus networks are 3x3 buildings + if (abs(node_x - nydus_x) <= 1 && abs(node_y - nydus_y) <= 1) return 1; + } + + return 0; +} + +typedef struct NydusInfo { + float distance_heuristic_to_nydus; + int closest_nydus_index; + uint8_t can_enter_nydus; + uint8_t point_belongs_to_nydus; +} NydusInfo; + +static inline NydusInfo get_node_nydus_info(int *nydus_nodes, int nydus_count, int node, int map_width, float baseline) +{ + float min_dist = HUGE_VALF; + NydusInfo info = { 0 }; + for (int i = 0; i < nydus_count; ++i) + { + int nydus_x = nydus_nodes[i] % map_width; + int nydus_y = nydus_nodes[i] / map_width; + int node_x = node % map_width; + int node_y = node / map_width; + float dist = distance_heuristic(node_x, node_y, nydus_x, nydus_y, baseline); + + if (dist < min_dist) + { + min_dist = dist; + info.distance_heuristic_to_nydus = dist; + info.closest_nydus_index = nydus_nodes[i]; + if (abs(node_x - nydus_x) <= 1 && abs(node_y - nydus_y) <= 1) + { + info.point_belongs_to_nydus = 1; + info.can_enter_nydus = 1; + break; + } + else if (abs(node_x - nydus_x) <= 2 && abs(node_y - nydus_y) <= 2) + { + info.can_enter_nydus = 1; + } + } + } + return info; +} + +typedef struct NydusPath +{ + int path_length; + int nydus_used_index; +} NydusPath; +/* +Run the astar algorithm. The resulting path is saved in paths +so each node knows the previous node and the path can be traced back. +Returns the path length. +*/ +static int run_pathfind_with_nydus(MemoryArena *arena, float *weights, int* paths, int w, int h, int start, int goal, int large, int* nydus_nodes, int nydus_count) +{ + float weight_baseline = find_min(weights, w*h); + + int path_length = -1; + + TempAllocation temp_alloc; + StartTemporaryAllocation(arena, &temp_alloc); + + PriorityQueue *nodes_to_visit = queue_create(arena, w*h); + + Node start_node = { start, 0.0f, 1 }; + float *costs = (float*) PushToMemoryArena(arena, w*h*sizeof(float)); + + for (int i = 0; i < w*h; ++i) + { + costs[i] = HUGE_VALF; + nodes_to_visit->index_map[i] = -1; + } + + costs[start] = 0; + + queue_push_or_update(nodes_to_visit, start_node); + + int nbrs[8]; + uint8_t nbr_fits[8]; + float nbr_costs[8] = { SQRT2, 1.0f, SQRT2, 1.0f, 1.0f, SQRT2, 1.0f, SQRT2 }; + + NydusInfo closest_nydus_to_goal = get_node_nydus_info(nydus_nodes, nydus_count, goal, w, weight_baseline); + + int *nydus_nbrs = PushToMemoryArena(arena, max_int(1, nydus_count - 1)*sizeof(int)); + + while (nodes_to_visit->size > 0) + { + Node cur = queue_pop(nodes_to_visit); + if (cur.idx == goal) + { + path_length = cur.path_length; + break; + } + + int row = cur.idx / w; + int col = cur.idx % w; + + if (cur.is_nydus) + { + int j = 0; + for (int i = 0; i < nydus_count; ++i) + { + if (nydus_nodes[i] != cur.idx) + { + nydus_nbrs[j] = nydus_nodes[i]; + j++; + } + } + + //Giving 8 places units can get out from a nydus on foot + //There may be some cases where some big unit like an ultralisk + //can't really fit and we should do proper checks + nbrs[UP_LEFT] = (row > 1 && col > 1) ? cur.idx - 2*w - 2 : -1; + nbrs[UP] = (row > 1) ? cur.idx - 2*w : -1; + nbrs[UP_RIGHT] = (row > 1 && col + 2 < w) ? cur.idx - 2*w + 2 : -1; + nbrs[LEFT] = (col > 1) ? cur.idx - 2 : -1; + nbrs[RIGHT] = (col + 2 < w) ? cur.idx + 2 : -1; + nbrs[DOWN_LEFT] = (row + 2 < h && col > 1) ? cur.idx + 2*w - 2 : -1; + nbrs[DOWN] = (row + 2 < h) ? cur.idx + 2*w : -1; + nbrs[DOWN_RIGHT] = (row + 2 < h && col + 2 < w) ? cur.idx + 2*w + 2 : -1; + + for (int i = 0; i < 8; ++i) + { + nbr_fits[i] = (nbrs[i] != -1 && weights[nbrs[i]] < HUGE_VALF) ? 1 : 0; + } + } + else + { + nbrs[UP_LEFT] = (row > 0 && col > 0) ? cur.idx - w - 1 : -1; + nbrs[UP] = (row > 0) ? cur.idx - w : -1; + nbrs[UP_RIGHT] = (row > 0 && col + 1 < w) ? cur.idx - w + 1 : -1; + nbrs[LEFT] = (col > 0) ? cur.idx - 1 : -1; + nbrs[RIGHT] = (col + 1 < w) ? cur.idx + 1 : -1; + nbrs[DOWN_LEFT] = (row + 1 < h && col > 0) ? cur.idx + w - 1 : -1; + nbrs[DOWN] = (row + 1 < h) ? cur.idx + w : -1; + nbrs[DOWN_RIGHT] = (row + 1 < h && col + 1 < w) ? cur.idx + w + 1 : -1; + + for (int i = 0; i < 8; ++i) + { + nbr_fits[i] = (nbrs[i] != -1 && weights[nbrs[i]] < HUGE_VALF) ? 1 : 0; + } + + if (large) + { + if (nbr_fits[UP]) + { + float up_left_weight = (nbrs[UP_LEFT] != -1) ? weights[nbrs[UP_LEFT]] : HUGE_VALF; + float up_right_weight = (nbrs[UP_RIGHT] != -1) ? weights[nbrs[UP_RIGHT]] : HUGE_VALF; + nbr_fits[UP] = (up_left_weight < HUGE_VALF || up_right_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[LEFT]) + { + float up_left_weight = (nbrs[UP_LEFT] != -1) ? weights[nbrs[UP_LEFT]] : HUGE_VALF; + float down_left_weight = (nbrs[DOWN_LEFT] != -1) ? weights[nbrs[DOWN_LEFT]] : HUGE_VALF; + + nbr_fits[LEFT] = (up_left_weight < HUGE_VALF || down_left_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[RIGHT]) + { + float down_right_weight = (nbrs[DOWN_RIGHT] != -1) ? weights[nbrs[DOWN_RIGHT]] : HUGE_VALF; + float up_right_weight = (nbrs[UP_RIGHT] != -1) ? weights[nbrs[UP_RIGHT]] : HUGE_VALF; + + nbr_fits[RIGHT] = (down_right_weight < HUGE_VALF || up_right_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[DOWN]) + { + float down_left_weight = (nbrs[DOWN_LEFT] != -1) ? weights[nbrs[DOWN_LEFT]] : HUGE_VALF; + float down_right_weight = (nbrs[DOWN_RIGHT] != -1) ? weights[nbrs[DOWN_RIGHT]] : HUGE_VALF; + + nbr_fits[DOWN] = (down_left_weight < HUGE_VALF || down_right_weight < HUGE_VALF) ? 1 : 0; + } + } + + if (nbr_fits[UP_LEFT]) + { + float up_weight = weights[nbrs[UP]]; + float left_weight = weights[nbrs[LEFT]]; + + nbr_fits[UP_LEFT] = (up_weight < HUGE_VALF && left_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[UP_RIGHT]) + { + float up_weight = weights[nbrs[UP]]; + float right_weight = weights[nbrs[RIGHT]]; + + nbr_fits[UP_RIGHT] = (up_weight < HUGE_VALF && right_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[DOWN_LEFT]) + { + float down_weight = weights[nbrs[DOWN]]; + float left_weight = weights[nbrs[LEFT]]; + + nbr_fits[DOWN_LEFT] = (down_weight < HUGE_VALF && left_weight < HUGE_VALF) ? 1 : 0; + } + + if (nbr_fits[DOWN_RIGHT]) + { + float down_weight = weights[nbrs[DOWN]]; + float right_weight = weights[nbrs[RIGHT]]; + + nbr_fits[DOWN_RIGHT] = (down_weight < HUGE_VALF && right_weight < HUGE_VALF) ? 1 : 0; + } + } + + float cur_cost = costs[cur.idx]; + + //Scaling the steps into and from nyduses with 4 + //Going into a nydus and getting out should take a bit more time than a single step on the grid + + for (int i = 0; i < 8; ++i) + { + if (nbr_fits[i]) + { + float new_cost; + if (cur.is_nydus) + { + new_cost = cur_cost + 4*weights[nbrs[i]] * nbr_costs[i]; + } + else + { + new_cost = cur_cost + weights[nbrs[i]] * nbr_costs[i]; + } + + //Small threshold to not update when the difference is just due to floating point inaccuracy + if (new_cost + 0.03f < costs[nbrs[i]]) + { + + float heuristic_cost = distance_heuristic(nbrs[i] % w, nbrs[i] / w, goal % w, goal / w, weight_baseline); + + if (nydus_count > 0) + { + NydusInfo closest_nydus = get_node_nydus_info(nydus_nodes, nydus_count, nbrs[i], w, weight_baseline); + + float heuristic_via_nydus = 4*weight_baseline + closest_nydus.distance_heuristic_to_nydus + closest_nydus_to_goal.distance_heuristic_to_nydus; + heuristic_cost = heuristic_cost < heuristic_via_nydus ? heuristic_cost : heuristic_via_nydus; + } + + float estimated_cost = new_cost + heuristic_cost; + Node new_node = { nbrs[i], estimated_cost, cur.path_length + 1, 0}; + queue_push_or_update(nodes_to_visit, new_node); + + costs[nbrs[i]] = new_cost; + paths[nbrs[i]] = cur.idx; + } + } + } + + if (cur.is_nydus) + { + for (int i = 0; i < nydus_count - 1; ++i) + { + float new_cost = cur_cost + 4*weight_baseline; + int nydus_nbr = nydus_nbrs[i]; + //Small threshold to not update when the difference is just due to floating point inaccuracy + if (new_cost + 0.03f < costs[nydus_nbr]) + { + float heuristic_cost = distance_heuristic(nydus_nbr % w, nydus_nbr / w, goal % w, goal / w, weight_baseline); + float heuristic_via_nydus = 4*weight_baseline + closest_nydus_to_goal.distance_heuristic_to_nydus; + heuristic_cost = heuristic_cost < heuristic_via_nydus ? heuristic_cost : heuristic_via_nydus; + + float estimated_cost = new_cost + heuristic_cost; + Node new_node = { nydus_nbr, estimated_cost, cur.path_length + 1, 1}; + queue_push_or_update(nodes_to_visit, new_node); + + costs[nydus_nbr] = new_cost; + paths[nydus_nbr] = cur.idx; + } + + } + } + else + { + NydusInfo closest_nydus = get_node_nydus_info(nydus_nodes, nydus_count, cur.idx, w, weight_baseline); + + if (nydus_count > 0 && closest_nydus.can_enter_nydus) + { + float heuristic_via_nydus = 4*weight_baseline + closest_nydus.distance_heuristic_to_nydus + closest_nydus_to_goal.distance_heuristic_to_nydus; + + float new_cost = cur_cost + 4*weight_baseline; + + if (new_cost + 0.03f < costs[closest_nydus.closest_nydus_index]) + { + float estimated_cost = new_cost + heuristic_via_nydus; + Node nydus_node = { closest_nydus.closest_nydus_index, estimated_cost, cur.path_length + 1, 1}; + + queue_push_or_update(nodes_to_visit, nydus_node); + + costs[closest_nydus.closest_nydus_index] = new_cost; + paths[closest_nydus.closest_nydus_index] = cur.idx; + } + } + } + } + + EndTemporaryAllocation(arena, &temp_alloc); + + return path_length; +} + /* Estimating a straight line weight over multiple nodes. Used in path smoothing where we remove nodes if we can jump @@ -801,6 +1118,43 @@ static float calculate_line_weight(MemoryArena *arena, float* weights, int w, in return weight_sum*norm; } +static VecInt* create_smoothed_path(MemoryArena *arena, float *weights, VecInt *complete_path, int start_index, int end_index, int w) +{ + int path_length = end_index - start_index; + + int start = complete_path->items[start_index]; + int goal = complete_path->items[end_index - 1]; + + VecInt *smoothed_path = InitVecInt(arena, path_length); + smoothed_path = PushToVecInt(smoothed_path, start); + int current_node = goal; + + float segment_total_weight = weights[goal] * distance_heuristic(goal % w, goal / w, current_node % w, current_node / w, 1.0f); + for (int i = 1; i < path_length - 1; ++i) + { + int current_node = complete_path->items[start_index + i]; + int next_node = complete_path->items[start_index + i + 1]; + float step_weight = weights[next_node] * distance_heuristic(current_node % w, current_node / w, next_node % w, next_node / w, 1.0f); + segment_total_weight += step_weight; + + int last_added_new_path_node = smoothed_path->items[smoothed_path->size - 1]; + int x0 = last_added_new_path_node % w; + int y0 = last_added_new_path_node / w; + int x1 = next_node % w; + int y1 = next_node / w; + + if (calculate_line_weight(arena, weights, w, x0, y0, x1, y1) > segment_total_weight * 1.002f) + { + segment_total_weight = step_weight; + smoothed_path = PushToVecInt(smoothed_path, current_node); + } + } + + smoothed_path = PushToVecInt(smoothed_path, goal); + + return smoothed_path; +} + /* Exported function to run astar from python. Takes in grid weights, dimensions of the grid, requested start and end @@ -853,31 +1207,7 @@ static PyObject* astar(PyObject *self, PyObject *args) current_node = paths[current_node]; } - VecInt *smoothed_path = InitVecInt(&state.function_arena, path_length); - smoothed_path = PushToVecInt(smoothed_path, start); - - float segment_total_weight = weights[goal] * distance_heuristic(goal % w, goal / w, current_node % w, current_node / w, 1.0f); - for (int i = 1; i < path_length - 1; ++i) - { - int current_node = complete_path->items[i]; - int next_node = complete_path->items[i + 1]; - float step_weight = weights[next_node] * distance_heuristic(current_node % w, current_node / w, next_node % w, next_node / w, 1.0f); - segment_total_weight += step_weight; - - int last_added_new_path_node = smoothed_path->items[smoothed_path->size - 1]; - int x0 = last_added_new_path_node % w; - int y0 = last_added_new_path_node / w; - int x1 = next_node % w; - int y1 = next_node / w; - - if (calculate_line_weight(&state.function_arena, weights, w, x0, y0, x1, y1) > segment_total_weight * 1.002f) - { - segment_total_weight = step_weight; - smoothed_path = PushToVecInt(smoothed_path, current_node); - } - } - - smoothed_path = PushToVecInt(smoothed_path, goal); + VecInt *smoothed_path = create_smoothed_path(&state.function_arena, weights, complete_path, 0, path_length, w); npy_intp dims[2] = {smoothed_path->size, 2}; PyArrayObject *path = (PyArrayObject*) PyArray_SimpleNew(2, dims, NPY_INT32); @@ -902,6 +1232,167 @@ static PyObject* astar(PyObject *self, PyObject *args) return return_val; } + +/* +Exported function to run astar with nyduses from python. +Takes in grid weights, dimensions of the grid, array with nydus positions as integer indices, requested start and end +and whether to smooth the final path. +*/ +static PyObject* astar_with_nydus(PyObject *self, PyObject *args) +{ + PyArrayObject* weights_object; + PyArrayObject* nydus_object; + int h, w, start, goal, large, smoothing, nydus_count; + + if (!PyArg_ParseTuple(args, "OiiOiiii", &weights_object, &h, &w, &nydus_object, &start, &goal, &large, &smoothing)) + { + return NULL; + } + + nydus_count = (int)nydus_object->dimensions[0]; + + float *weights = (float *)weights_object->data; + int *paths = (int*) PushToMemoryArena(&state.function_arena, w*h*sizeof(int)); + int *nydus_positions = (int*)nydus_object->data; + int path_length; + + if (nydus_count > 1) + { + path_length = run_pathfind_with_nydus(&state.function_arena, weights, paths, w, h, start, goal, large, nydus_positions, nydus_count); + } + else + { + path_length = run_pathfind(&state.function_arena, weights, paths, w, h, start, goal, large); + } + + PyObject *return_val; + if (path_length >= 0) + { + int current_index = goal; + int nydus_index = -1; + + VecInt *complete_path = InitVecInt(&state.function_arena, path_length); + complete_path->size = path_length; + + for (int i = path_length - 1; i >= 0; --i) + { + for (int j = 0; j < nydus_count; ++j) + { + if (nydus_positions[j] == current_index) + { + nydus_index = i; + } + } + complete_path->items[i] = current_index; + current_index = paths[current_index]; + } + + if (nydus_index == -1) + { + return_val = PyList_New(1); + if (!smoothing || path_length < 3) + { + npy_intp dims[2] = {path_length, 2}; + PyArrayObject *path = (PyArrayObject*) PyArray_SimpleNew(2, dims, NPY_INT32); + npy_int32 *path_data = (npy_int32*)path->data; + + int idx = goal; + + for (npy_intp i = dims[0] - 1; i >= 0; --i) + { + path_data[2*i] = idx / w; + path_data[2*i + 1] = idx % w; + + idx = paths[idx]; + } + PyList_SetItem(return_val, 0, PyArray_Return(path)); + } + else + { + VecInt *smoothed_path = create_smoothed_path(&state.function_arena, weights, complete_path, 0, path_length, w); + + npy_intp dims[2] = {smoothed_path->size, 2}; + PyArrayObject *path = (PyArrayObject*) PyArray_SimpleNew(2, dims, NPY_INT32); + npy_int32 *path_data = (npy_int32*)path->data; + + for (npy_intp i = 0; i < dims[0]; ++i) + { + path_data[2*i] = smoothed_path->items[i] / w; + path_data[2*i + 1] = smoothed_path->items[i] % w; + } + PyList_SetItem(return_val, 0, PyArray_Return(path)); + } + } + else + { + return_val = PyList_New(2); + + if (!smoothing) + { + npy_intp dims1[2] = {nydus_index + 1, 2}; + PyArrayObject *path1 = (PyArrayObject*) PyArray_SimpleNew(2, dims1, NPY_INT32); + npy_int32 *path1_data = (npy_int32*)path1->data; + + for (npy_intp i = 0; i <= nydus_index; ++i) + { + path1_data[2*i] = complete_path->items[i] / w; + path1_data[2*i + 1] = complete_path->items[i] % w; + } + PyList_SetItem(return_val, 0, PyArray_Return(path1)); + + npy_intp dims2[2] = {path_length - (nydus_index + 1), 2}; + PyArrayObject *path2 = (PyArrayObject*) PyArray_SimpleNew(2, dims2, NPY_INT32); + npy_int32 *path2_data = (npy_int32*)path2->data; + + for (npy_intp i = nydus_index + 1; i < path_length; ++i) + { + int index_to_set = i - (nydus_index + 1); + path2_data[2*index_to_set] = complete_path->items[i] / w; + path2_data[2*index_to_set + 1] = complete_path->items[i] % w; + } + PyList_SetItem(return_val, 1, PyArray_Return(path2)); + } + else + { + VecInt *smoothed_path1 = create_smoothed_path(&state.function_arena, weights, complete_path, 0, nydus_index + 1, w); + + npy_intp dims1[2] = {smoothed_path1->size, 2}; + PyArrayObject *path1 = (PyArrayObject*) PyArray_SimpleNew(2, dims1, NPY_INT32); + npy_int32 *path1_data = (npy_int32*)path1->data; + + for (npy_intp i = 0; i < dims1[0]; ++i) + { + path1_data[2*i] = smoothed_path1->items[i] / w; + path1_data[2*i + 1] = smoothed_path1->items[i] % w; + } + PyList_SetItem(return_val, 0, PyArray_Return(path1)); + + VecInt *smoothed_path2 = create_smoothed_path(&state.function_arena, weights, complete_path, nydus_index + 1, path_length, w); + + npy_intp dims2[2] = {smoothed_path2->size, 2}; + PyArrayObject *path2 = (PyArrayObject*) PyArray_SimpleNew(2, dims2, NPY_INT32); + npy_int32 *path2_data = (npy_int32*)path2->data; + + for (npy_intp i = 0; i < dims2[0]; ++i) + { + path2_data[2*i] = smoothed_path2->items[i] / w; + path2_data[2*i + 1] = smoothed_path2->items[i] % w; + } + PyList_SetItem(return_val, 1, PyArray_Return(path2)); + } + } + } + else + { + return_val = Py_BuildValue(""); + } + + ClearMemoryArena(&state.function_arena); + ClearMemoryArena(&state.temp_arena); + + return return_val; +} + typedef struct KeyContainer { VecInt *keys; @@ -1758,6 +2249,7 @@ static PyObject* get_map_data(PyObject *self, PyObject *args) static PyMethodDef cext_methods[] = { {"astar", (PyCFunction)astar, METH_VARARGS, "astar"}, + {"astar_with_nydus", (PyCFunction)astar_with_nydus, METH_VARARGS, "astar_with_nydus"}, {"get_map_data", (PyCFunction)get_map_data, METH_VARARGS, "get_map_data"}, {NULL, NULL, 0, NULL} }; diff --git a/MapAnalyzer/cext/wrapper.py b/MapAnalyzer/cext/wrapper.py index 25c1519..afbd3dc 100644 --- a/MapAnalyzer/cext/wrapper.py +++ b/MapAnalyzer/cext/wrapper.py @@ -1,9 +1,9 @@ import numpy as np try: - from .mapanalyzerext import astar as ext_astar, get_map_data as ext_get_map_data + from .mapanalyzerext import astar as ext_astar, astar_with_nydus as ext_astar_nydus, get_map_data as ext_get_map_data except ImportError: - from mapanalyzerext import astar as ext_astar, get_map_data as ext_get_map_data + from mapanalyzerext import astar as ext_astar, astar_with_nydus as ext_astar_nydus, get_map_data as ext_get_map_data from typing import Optional, Tuple, Union, List, Set from sc2.position import Point2, Rect @@ -53,6 +53,7 @@ def __repr__(self) -> str: ] } + def astar_path( weights: np.ndarray, start: Tuple[int, int], @@ -77,9 +78,46 @@ def astar_path( height, width = weights.shape start_idx = np.ravel_multi_index(start, (height, width)) goal_idx = np.ravel_multi_index(goal, (height, width)) + path = ext_astar( weights.flatten(), height, width, start_idx, goal_idx, large, smoothing ) + + return path + +def astar_path_with_nyduses(weights: np.ndarray, + start: Tuple[int, int], + goal: Tuple[int, int], + nydus_positions: List[Point2], + large: bool = False, + smoothing: bool = False) -> Union[List[np.ndarray], None]: + # For the heuristic to be valid, each move must have a positive cost. + # Demand costs above 1 so floating point inaccuracies aren't a problem + # when comparing costs + if weights.min(axis=None) < 1: + raise ValueError("Minimum cost to move must be above or equal to 1, but got %f" % ( + weights.min(axis=None))) + # Ensure start is within bounds. + if (start[0] < 0 or start[0] >= weights.shape[0] or + start[1] < 0 or start[1] >= weights.shape[1]): + raise ValueError(f"Start of {start} lies outside grid.") + # Ensure goal is within bounds. + if (goal[0] < 0 or goal[0] >= weights.shape[0] or + goal[1] < 0 or goal[1] >= weights.shape[1]): + raise ValueError(f"Goal of {goal} lies outside grid.") + + height, width = weights.shape + start_idx = np.ravel_multi_index(start, (height, width)) + goal_idx = np.ravel_multi_index(goal, (height, width)) + nydus_array = np.zeros((len(nydus_positions),), dtype=np.int32) + + for index, pos in enumerate(nydus_positions): + nydus_idx = np.ravel_multi_index((int(pos.x), int(pos.y)), (height, width)) + nydus_array[index] = nydus_idx + + path = ext_astar_nydus(weights.flatten(), height, width, nydus_array.flatten(), + start_idx, goal_idx, large, smoothing) + return path diff --git a/package-lock.json b/package-lock.json index 15dc2cc..4ecbebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sc2mapanalyzer", - "version": "0.0.84", + "version": "0.0.85", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b48bfd2..3420b76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sc2mapanalyzer", - "version": "0.0.84", + "version": "0.0.85", "dependencies": { "update": "^0.7.4" } diff --git a/pf_perf.py b/pf_perf.py index de4d424..8b022e9 100644 --- a/pf_perf.py +++ b/pf_perf.py @@ -55,17 +55,16 @@ def get_map_file_list() -> List[str]: for p in pts: arr = map_data.add_cost(p, r, arr) -start = time.perf_counter() -path = map_data.pathfind_pyastar(p0, p1, grid=arr, allow_diagonal=True) -pyastar_time = time.perf_counter() - start -print("pyastar time: {}".format(pyastar_time)) - -map_data.plot_influenced_path_pyastar(start=p0, goal=p1, weight_array=arr, allow_diagonal=True) - start = time.perf_counter() path2 = map_data.pathfind(p0, p1, grid=arr) ext_time = time.perf_counter() - start print("extension astar time: {}".format(ext_time)) -print("div: {}".format(ext_time / pyastar_time)) + +start = time.perf_counter() +nydus_path = map_data.pathfind_with_nyduses(p0, p1, grid=arr) +nydus_time = time.perf_counter() - start +print("nydus astar time: {}".format(nydus_time)) +print("compare to without nydus: {}".format(nydus_time / ext_time)) map_data.plot_influenced_path(start=p0, goal=p1, weight_array=arr) +map_data.plot_influenced_path_nydus(start=p0, goal=p1, weight_array=arr) diff --git a/requirements.txt b/requirements.txt index 85b0bc8..7be968e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -371,7 +371,7 @@ numba== 0.52.0 # D:\proj\SC2MapAnalysis\MapAnalyzer\utils.py: 4 # D:\proj\SC2MapAnalysis\setup.py: 14 # D:\proj\SC2MapAnalysis\tests\test_c_extension.py: 2 -numpy== 1.19.3 +numpy # D:\proj\SC2MapAnalysis\.eggs\numpy-1.19.1-py3.7-win-amd64.egg\numpy\_pytesttester.py: 125 diff --git a/setup.py b/setup.py index 78cb1d5..cdb1d6a 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,8 @@ def finalize_options(self): requirements = [ # pragma: no cover "wheel", - "numpy==1.19.3", + "numpy", "Cython", - "pyastar@git+git://github.com/eladyaniv01/pyastar.git@master#egg=pyastar", "burnysc2", "matplotlib", "scipy", @@ -33,9 +32,9 @@ def finalize_options(self): setup( # pragma: no cover name="sc2mapanalyzer", # version=f"{__version__}", - version="0.0.84", + version="0.0.85", install_requires=requirements, - setup_requires=["wheel", "numpy==1.19.3"], + setup_requires=["wheel", "numpy"], cmdclass={"build_ext": build_ext}, ext_modules=[mapping_module], packages=["MapAnalyzer", "MapAnalyzer.cext"], diff --git a/tests/test_c_extension.py b/tests/test_c_extension.py index dbd8506..79afe59 100644 --- a/tests/test_c_extension.py +++ b/tests/test_c_extension.py @@ -1,6 +1,6 @@ -from MapAnalyzer.cext import CMapInfo, astar_path +from MapAnalyzer.cext import CMapInfo, astar_path, astar_path_with_nyduses import numpy as np -from sc2.position import Rect +from sc2.position import Rect, Point2 import os @@ -43,6 +43,23 @@ def test_c_extension(): assert(path2 is not None and path2.shape[0] == 59) + paths_no_nydus = astar_path_with_nyduses(influenced_grid, (3, 3), (33, 38), [], False, False) + + assert(paths_no_nydus is not None and paths_no_nydus[0].shape[0] == 59) + + nydus_positions = [ + Point2((6.5, 6.5)), + Point2((29.5, 34.5)) + ] + + influenced_grid[5:8, 5:8] = np.inf + influenced_grid[28:31, 33:36] = np.inf + + paths_nydus = astar_path_with_nyduses(influenced_grid, (3, 3), (33, 38), nydus_positions, False, False) + + assert (paths_nydus is not None and len(paths_nydus) == 2 + and len(paths_nydus[0]) + len(paths_nydus[1]) == 7) + height_map = np.where(walkable_grid == 0, 24, 8).astype(np.uint8) playable_area = Rect([1, 1, 38, 38])