Преглед на файлове

rewrite pathfinding.path

olivier.massot преди 8 години
родител
ревизия
c7886c6064
променени са 3 файла, в които са добавени 86 реда и са изтрити 118 реда
  1. 25 16
      pypog/geometry_objects.py
  2. 1 2
      pypog/grid_objects.py
  3. 60 100
      pypog/pathfinding.py

+ 25 - 16
pypog/geometry_objects.py

@@ -15,8 +15,8 @@ except ImportError:
 class BoundingRect(tuple):
     """ Bounding rectangle defined by a top-left (xmin, ymin) point
      and a bottom-right (xmax, ymax) point """
-    def __new__(self, xmin=-inf, ymin=-inf, xmax=inf, ymax=inf):
-        return tuple.__new__(self, (xmin, ymin, xmax, ymax))
+    def __new__(cls, xmin=-inf, ymin=-inf, xmax=inf, ymax=inf):
+        return tuple.__new__(cls, (xmin, ymin, xmax, ymax))
 
     @classmethod
     def from_(cls, *args):
@@ -194,6 +194,10 @@ class BaseGeometry:
         """ distance between 1 and 2 (run faster than a standard distance) """
         return (x1 - x2) ** 2 + (y1 - y2) ** 2
 
+    @staticmethod
+    def manhattan(xa, ya, xb, yb):
+        """returns the manhattan distance between the two cells"""
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
 class SquareGeometry(BaseGeometry):
     """ Geometry on square grids """
@@ -371,6 +375,11 @@ class SquareGeometry(BaseGeometry):
             result.append((xr, yr))
         return result
 
+    @staticmethod
+    def manhattan(xa, ya, xb, yb):
+        """ reimplemented from BaseGeometry.manhattan """
+        return abs(xa - xb) + abs(ya - yb)
+
 class HexGeometry(BaseGeometry):
     """ Base class for hexagonal grids classes
     This class should be overridden """
@@ -403,15 +412,15 @@ class HexGeometry(BaseGeometry):
         return (rx, ry, rz)
 
     @staticmethod
-    def cubic_distance(*args):
-        """returns the manhattan distance between the two cells,
+    def manhattan(*args):
+        """ reimplemented from BaseGeometry.manhattan,
         using cubic coordinates"""
         try:
             xa, ya, za, xb, yb, zb = args
-            return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
+            return abs(xa - xb) + abs(ya - yb) + abs(za - zb)
         except ValueError:
             xa, ya, xb, yb = args
-            HexGeometry.cubic_distance(*HexGeometry.to_cubic(xa, ya), *HexGeometry.to_cubic(xb, yb))
+            return HexGeometry.manhattan(*HexGeometry.to_cubic(xa, ya), *HexGeometry.to_cubic(xb, yb))
 
 class FHexGeometry(HexGeometry):
     """ Flat-hexagonal grid object """
@@ -549,8 +558,8 @@ class FHexGeometry(HexGeometry):
         result = []
 
         # convert to cubic coodinates (see 'cube_coords' lib)
-        xua, yua, _ = cls.cv_off_cube(xa, ya)
-        xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
+        xua, yua, _ = cls.to_cubic(xa, ya)
+        xuh, yuh, zuh = cls.to_cubic(xh, yh)
 
         # direction vector
         dx_dir, dy_dir = xuh - xua, yuh - yua
@@ -567,8 +576,8 @@ class FHexGeometry(HexGeometry):
         xub, yub, zub = cls.cube_round(xub, yub, zub)
         xuc, yuc, zuc = cls.cube_round(xuc, yuc, zuc)
 
-        xb, yb = cls.cv_cube_off(xub, yub, zub)
-        xc, yc = cls.cv_cube_off(xuc, yuc, zuc)
+        xb, yb = cls.from_cubic(xub, yub, zub)
+        xc, yc = cls.from_cubic(xuc, yuc, zuc)
 
         # sides
         segments = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
@@ -610,8 +619,8 @@ class FHexGeometry(HexGeometry):
         k = 1 / (iAngle * sqrt(3))
 
         # use cubic coordinates
-        xua, yua, zua = cls.cv_off_cube(xa, ya)
-        xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
+        xua, yua, zua = cls.to_cubic(xa, ya)
+        xuh, yuh, zuh = cls.to_cubic(xh, yh)
 
         length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
 
@@ -624,7 +633,7 @@ class FHexGeometry(HexGeometry):
 
         # this is approximative: height is update according to the manhattan distance to center
         for x, y in flat_triangle:
-            xu, yu, zu = cls.cv_off_cube(x, y)
+            xu, yu, zu = cls.to_cubic(x, y)
             distance = int(max(abs(xu - xua), abs(yu - yua), abs(zu - zua)))
             try:
                 z_list = vertical_line_dict[ distance ]
@@ -643,15 +652,15 @@ class FHexGeometry(HexGeometry):
         if coordinates == [center] or rotations % 6 == 0:
             return coordinates
         x0, y0 = center
-        xu0, yu0, zu0 = cls.cv_off_cube(x0, y0)
+        xu0, yu0, zu0 = cls.to_cubic(x0, y0)
         result = []
 
         for x, y in coordinates:
-            xu, yu, zu = cls.cv_off_cube(x, y)
+            xu, yu, zu = cls.to_cubic(x, y)
             dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
             for _ in range(rotations):
                 dxu, dyu, dzu = -dzu, -dxu, -dyu
             xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
-            xr, yr = cls.cv_cube_off(xru, yru, zru)
+            xr, yr = cls.from_cubic(xru, yru, zru)
             result.append((xr, yr))
         return result

+ 1 - 2
pypog/grid_objects.py

@@ -5,7 +5,6 @@
 '''
 from pypog.geometry_objects import BaseGeometry, FHexGeometry, SquareGeometry, \
     BoundingRect, HexGeometry
-from pypog.painter_objects import LinePainter
 
 
 class BaseGrid(object):
@@ -126,7 +125,7 @@ class BaseGrid(object):
         return True
 
     # pathfinding
-    def _movingcost(self, from_x, from_y, to_x, to_y):
+    def movingcost(self, from_x, from_y, to_x, to_y):
         return 1
 
 class SquareGrid(BaseGrid):

+ 60 - 100
pypog/pathfinding.py

@@ -1,133 +1,93 @@
 '''
-   Implement the A* algorithm
+   Implements the A* algorithm
 
-   Use the path function like that:
+   usage:
+       grid = SquareGrid(30, 30)
+       p = path(grid, (1, 6), (3, 9))
+       >> [(2, 7), (3, 8), (3, 9), (3, 9)]
 
-       path(my_grid, (xs, ys), (xt, yt), my_moving_cost_function)
-       >> [(xs, ys), (x1, y1), (x2, y2), ...(xt, yt)]
-
-       where:
-        - my_grid is a Grid, HexGrid, or SquareGrid object
-        - (xs, ys) is the starting cell
-        - (xt, yt) is the targeted cell
-        - my_moving_cost_function is a pointer to your custom function. This function should be like:
-
-        def my_moving_cost_function((x0, y0), (x1, y1)):
-            ...
-            return cost
-
-        this function should return an INTEGER which represent the cost of a move from (x0, y0) to (x1, y1),
-        where (x0, y0) and (x1, y1) are adjacent cells
-
-        If cost is negative, move is impossible.
-        If move is strictly positive, it represents the difficulty to move from 0 to 1:
-        the returned path will be the easiest from (xs, ys) to (xt, yt)
-
-    3D paths:
-        The path method takes account of the differents altitudes of the cells, but it is not designed to
-        work for a flying mover.
-        More clearly: the path will be on the ground: walking, climbing, but no flying for instance.
+    * 'grid': Grid object
+    * 'origin' starting (x, y) coordinates
+    * 'target' targeted (x, y) coordinates
 
     ** By Cro-Ki l@b, 2017 **
 '''
-from pypog.geometry_objects import HexGeometry
-from pypog.grid_objects import BaseGrid
+import heapq
 
 class NoPathFound(Exception):
     pass
 
-class Node():
-    target = None
-    def __init__(self, x, y, parent=None):
-        self._x = x
-        self._y = y
-        self.parent = parent
-        self.gcost = 0
-        self.hcost = 0
-
-    def compute(self, moving_cost):
-        # the manhattan distance to the final target
-        self.hcost = HexGeometry.cubic_distance(self._x, self._y, *self.target)
-
-        # the cumulated moving cost of the path that lead here
-        self.gcost = self.parent.g_cost + self.moving_cost
-
-    @property
-    def cost(self):
-        return self.g_cost + self.h_cost
+class Node(tuple):
+    def __new__(self, x, y, parent=None):
+        n = tuple.__new__(self, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
 
-def path(grid, from_x, from_y, to_x, to_y):
-    """return the shorter path from origin to target on the Grid object
-    the path is estimated following:
-    - geometry of the grid
-    - altitudes of the cells
-    - cost of the move returned by the 'moving_cost_function'
+def path(grid, origin, target, include_origin):
 
-    origin and target should be Cell objects
-    """
-    if not isinstance(grid, BaseGrid):
-        raise TypeError("grid has to be an instance of BaseGrid (given: {})".format(type(grid).__name__))
+    # list of checked nodes
+    nodes = []
 
-    nodes = {}
+    # starting node, cost is 0 and parent is None
+    origin = Node(*origin)
 
-    # pass target to the Node class:
-    Node.target = (to_x, to_y)
+    # append 'origin' to nodes, with priority 0
+    heapq.heappush(nodes, (0, origin))
 
-    # origin node
-    nO = Node(from_x, from_y)
+    # while there are unchecked nodes , process
+    while nodes:
 
-    # current position
-    pos = nO
+        # pop the node with the lowest priority (cost) from the list,
+        current = heapq.heappop(nodes)[1]
 
-    while (pos.x, pos.y) != (to_x, to_y):
+        # early exit
+        if current == target:
+            break
 
-        # lists the neighbors of the current position
-        neighbours = grid.neighbors(pos.x, pos.y)
+        for x, y in grid.neighbors(*current):
 
+            node = Node(x, y, current)
 
-
-        # removes the coordinates already checked
-        neighbours = set(neighbours) - set(nodes.keys())
-
-        for x, y in neighbours:
-
-            # use the grid's movingcost() function to get the moving cost from position to (x, y)
-            cost = grid._movingcost(pos.x, pos.y, x, y)
-
-            # cost is negative, can not go there
-            if cost < 0:
+            # get the moving cost to this node
+            movingcost = grid.movingcost(*current, *node)
+            if movingcost < 0:
                 continue
 
-            # instanciate the new node with 'pos' as parent
-            node = Node(x, y, pos)
-            node.compute(cost)
+            # cost of the node is the accumulated cost from origin
+            node.cost = current.cost + movingcost
 
             # check if there is already a node with a lower cost
             try:
-                if nodes[(x, y)].cost <= node.cost:
+                index = nodes.index(node)
+                if nodes[index].cost > node.cost:
+                    del nodes[index]
+                else:
                     continue
-            except KeyError:
+            except ValueError:
                 pass
 
-            # memorize the node
-            nodes[(x, y)] = node
+            # compute the cost of the node
+            priority = node.cost + grid.geometry.manhattan(*node, *target)
 
-        # no new nodes were found
-        if not nodes:
-            raise NoPathFound()
-
-        # retrieves the lowest cost
-        best = min(nodes.values(), key=lambda x: x.cost)
+            # append to the checked nodes list
+            heapq.heappush(nodes, (priority, node))
+    else:
+        # all the reachable nodes hve been checked, no way found to the target
+        raise NoPathFound("no path were found to the targetted location {}".format(target))
 
-        del nodes[best.coord]
-        pos = best
+    # build the result
+    result = [target]
+    while current != origin:
+        result.append(tuple(current))
+        current = current.parent
+    result.reverse()
 
-    else:
+    return result
 
-        # build the result
-        path = []
-        while (pos.x, pos.y) != (from_x, from_y):
-            path.insert(0, (pos.x, pos.y, pos.k_dep))
-            pos = pos.parent
+if __name__ == '__main__':
+    from pypog.grid_objects import SquareGrid
+    grid = SquareGrid(30, 30)
+    p = path(grid, (1, 6), (3, 9))
+    print(p)
 
-    return path