Issues (838)

tcod/path.py (10 issues)

1
"""
2
3
Example::
4
5
    >>> import numpy as np
6
    >>> import tcod.path
7
    >>> dungeon = np.array(
8
    ...     [
9
    ...         [1, 0, 1, 1, 1],
10
    ...         [1, 0, 1, 0, 1],
11
    ...         [1, 1, 1, 0, 1],
12
    ...     ],
13
    ...     dtype=np.int8,
14
    ...     )
15
    ...
16
    >>> height, width = dungeon.shape
17
18
    # Create a pathfinder from a numpy array.
19
    # This is the recommended way to use the tcod.path module.
20
    >>> astar = tcod.path.AStar(dungeon)
21
    >>> print(astar.get_path(0, 0, 4, 2))
22
    [(0, 1), (1, 2), (2, 1), (3, 0), (4, 1), (4, 2)]
23
    >>> astar.cost[0, 1] = 1 # You can access the map array via this attribute.
24
    >>> print(astar.get_path(0, 0, 4, 2))
25
    [(1, 0), (2, 0), (3, 0), (4, 1), (4, 2)]
26
27
    # Create a pathfinder from an edge_cost function.
28
    # Calling Python functions from C is known to be very slow.
29
    >>> def edge_cost(my_x, my_y, dest_x, dest_y):
30
    ...     return dungeon[dest_y, dest_x]
31
    ...
32
    >>> dijkstra = tcod.path.Dijkstra(
33
    ...     tcod.path.EdgeCostCallback(edge_cost, width, height),
34
    ...     )
35
    ...
36
    >>> dijkstra.set_goal(0, 0)
37
    >>> print(dijkstra.get_path(4, 2))
38
    [(1, 0), (2, 0), (3, 0), (4, 1), (4, 2)]
39
"""
40
41
from __future__ import absolute_import
42
43
import numpy as np
44
45
from tcod.libtcod import lib, ffi
46
47
48
@ffi.def_extern()
49
def _pycall_path_old(x1, y1, x2, y2, handle):
50
    """libtcodpy style callback, needs to preserve the old userData issue."""
51
    func, userData = ffi.from_handle(handle)
52
    return func(x1, y1, x2, y2, userData)
53
54
55
@ffi.def_extern()
56
def _pycall_path_simple(x1, y1, x2, y2, handle):
57
    """Does less and should run faster, just calls the handle function."""
58
    return ffi.from_handle(handle)(x1, y1, x2, y2)
59
60
61
@ffi.def_extern()
62
def _pycall_path_swap_src_dest(x1, y1, x2, y2, handle):
63
    """A TDL function dest comes first to match up with a dest only call."""
64
    return ffi.from_handle(handle)(x2, y2, x1, y1)
65
66
67
@ffi.def_extern()
68
def _pycall_path_dest_only(x1, y1, x2, y2, handle):
69
    """A TDL function which samples the dest coordinate only."""
70
    return ffi.from_handle(handle)(x2, y2)
71
72
def _get_pathcost_func(name):
73
    """Return a properly cast PathCostArray callback."""
74
    return ffi.cast('TCOD_path_func_t', ffi.addressof(lib, name))
75
76
77
class _EdgeCostFunc(object):
78
    """
79
    Args:
80
        userdata (Any): Custom userdata to send to the C call.
81
        width (int): The maximum width of this callback.
82
        height (int): The maximum height of this callback.
83
    """
84
    _CALLBACK_P = lib._pycall_path_old
85
86
    def __init__(self, userdata, width, height):
87
        self._userdata = userdata
88
        self.width = width
89
        self.height = height
90
91
    def get_tcod_path_ffi(self):
92
        """
93
94
        Returns:
95
            Tuple[CData, CData]: A cffi (callback, userdata) pair.
96
97
98
        """
99
        return (self._CALLBACK_P, ffi.new_handle(self._userdata),
100
                self.width, self.height)
101
102
    def __repr__(self):
103
        return '%s(%r, width=%r, height=%r)' % (
104
            self.__class__.__name__,
105
            self._userdata, self.width, self.height,
106
            )
107
108
109
class EdgeCostCallback(_EdgeCostFunc):
110
    """Calculate cost from an edge-cost callback.
111
112
    Args:
113
        callback (Callable[[int, int, int, int], float]):
114
            A callback which can handle the following parameters:
115
            `(source_x:int, source_y:int, dest_x:int, dest_y:int) -> float`
116
        width (int): The maximum width of this callback.
117
        height (int): The maximum height of this callback.
118
    """
119
    _CALLBACK_P = lib._pycall_path_simple
120
121
    def __init__(self, callback, width, height):
122
        self.callback = callback
123
        super(EdgeCostCallback, self).__init__(callback, width, height)
124
125
class NodeCostArray(np.ndarray):
126
    """Calculate cost from a numpy array of nodes.
127
128
    Args:
129
        array (numpy.ndarray): A numpy array with the cost of each node.
130
    """
131
132
    _C_ARRAY_CALLBACKS = {
133
        np.float32: ('float*', _get_pathcost_func('PathCostArrayFloat32')),
134
        np.bool_: ('int8_t*', _get_pathcost_func('PathCostArrayInt8')),
135
        np.int8: ('int8_t*', _get_pathcost_func('PathCostArrayInt8')),
136
        np.uint8: ('uint8_t*', _get_pathcost_func('PathCostArrayUInt8')),
137
        np.int16: ('int16_t*', _get_pathcost_func('PathCostArrayInt16')),
138
        np.uint16: ('uint16_t*', _get_pathcost_func('PathCostArrayUInt16')),
139
        np.int32: ('int32_t*', _get_pathcost_func('PathCostArrayInt32')),
140
        np.uint32: ('uint32_t*', _get_pathcost_func('PathCostArrayUInt32')),
141
        }
142
143
    def __new__(cls, array):
144
        """Validate a numpy array and setup a C callback."""
145
        self = np.ascontiguousarray(array).view(cls)
146
        return self
147
148
    @property
149
    def width(self):
150
        return self.shape[1]
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named shape.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
151
152
    @property
153
    def height(self):
154
        return self.shape[0]
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named shape.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
155
156
    def __repr__(self):
157
        return '%s(%r)' % (self.__class__.__name__,
158
                           repr(self.view(np.ndarray)))
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named view.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
159
160
    def get_tcod_path_ffi(self):
161
        if len(self.shape) != 2:
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named shape.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
162
            raise ValueError('Array must have a 2d shape, shape is %r' %
163
                             (self.shape,))
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named shape.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
164
        if self.dtype.type not in self._C_ARRAY_CALLBACKS:
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named dtype.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
165
            raise ValueError('dtype must be one of %r, dtype is %r' %
166
                             (self._C_ARRAY_CALLBACKS.keys(), self.dtype.type))
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named dtype.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
167
168
        array_type, callback = \
169
            self._C_ARRAY_CALLBACKS[self.dtype.type]
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named dtype.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
170
        userdata = ffi.new(
171
            'PathCostArray*',
172
            (self.width, ffi.cast(array_type, self.ctypes.data)),
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named ctypes.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
173
        )
174
        return callback, userdata, self.width, self.height
175
176
177
class _PathFinder(object):
178
    """A class sharing methods used by AStar and Dijkstra."""
179
180
    def __init__(self, cost, diagonal=1.41):
181
        self.cost = cost
182
        self.diagonal = diagonal
183
        self._path_c = None
184
        self._callback = self._userdata = None
185
186
        if hasattr(self.cost, 'map_c'):
187
            self.width, self.height = self.cost.width, self.cost.height
188
            self._path_c = ffi.gc(
189
                self._path_new_using_map(self.cost.map_c, diagonal),
0 ignored issues
show
The Instance of NodeCostArray does not seem to have a member named map_c.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
190
                self._path_delete,
191
                )
192
            return
193
194
        if not hasattr(self.cost, 'get_tcod_path_ffi'):
195
            assert not callable(self.cost), \
196
                "Any callback alone is missing width & height information. " \
197
                "Wrap your callback in tcod.path.EdgeCostCallback"
198
            self.cost = NodeCostArray(self.cost)
199
200
        self._callback, self._userdata, self.width, self.height = \
201
            self.cost.get_tcod_path_ffi()
202
        self._path_c = ffi.gc(
203
            self._path_new_using_function(
204
                self.width,
205
                self.height,
206
                self._callback,
207
                self._userdata,
208
                diagonal
209
                ),
210
            self._path_delete,
211
            )
212
213
    def __repr__(self):
214
        return '%s(cost=%r, diagonal=%r)' % (self.__class__.__name__,
215
                                             self.cost, self.diagonal)
216
217
    def __getstate__(self):
218
        state = self.__dict__.copy()
219
        del state['_path_c']
220
        del state['width']
221
        del state['height']
222
        del state['_callback']
223
        del state['_userdata']
224
        return state
225
226
    def __setstate__(self, state):
227
        self.__dict__.update(state)
228
        self.__init__(self.cost, self.diagonal)
229
230
    _path_new_using_map = lib.TCOD_path_new_using_map
231
    _path_new_using_function = lib.TCOD_path_new_using_function
232
    _path_delete = lib.TCOD_path_delete
233
234
235
class AStar(_PathFinder):
236
    """
237
    Args:
238
        cost (Union[tcod.map.Map, numpy.ndarray, Any]):
239
        diagonal (float): Multiplier for diagonal movement.
240
            A value of 0 will disable diagonal movement entirely.
241
    """
242
243
    def get_path(self, start_x, start_y, goal_x, goal_y):
244
        """Return a list of (x, y) steps to reach the goal point, if possible.
245
246
        Args:
247
            start_x (int): Starting X position.
248
            start_y (int): Starting Y position.
249
            goal_x (int): Destination X position.
250
            goal_y (int): Destination Y position.
251
        Returns:
252
            List[Tuple[int, int]]:
253
                A list of points, or an empty list if there is no valid path.
254
        """
255
        lib.TCOD_path_compute(self._path_c, start_x, start_y, goal_x, goal_y)
256
        path = []
257
        x = ffi.new('int[2]')
258
        y = x + 1
259
        while lib.TCOD_path_walk(self._path_c, x, y, False):
260
            path.append((x[0], y[0]))
261
        return path
262
263
264
class Dijkstra(_PathFinder):
265
    """
266
    Args:
267
        cost (Union[tcod.map.Map, numpy.ndarray, Any]):
268
        diagonal (float): Multiplier for diagonal movement.
269
            A value of 0 will disable diagonal movement entirely.
270
    """
271
272
    _path_new_using_map = lib.TCOD_dijkstra_new
273
    _path_new_using_function = lib.TCOD_dijkstra_new_using_function
274
    _path_delete = lib.TCOD_dijkstra_delete
275
276
    def set_goal(self, x, y):
277
        """Set the goal point and recompute the Dijkstra path-finder.
278
        """
279
        lib.TCOD_dijkstra_compute(self._path_c, x, y)
280
281
    def get_path(self, x, y):
282
        """Return a list of (x, y) steps to reach the goal point, if possible.
283
        """
284
        lib.TCOD_dijkstra_path_set(self._path_c, x, y)
285
        path = []
286
        pointer_x = ffi.new('int[2]')
287
        pointer_y = pointer_x + 1
288
        while lib.TCOD_dijkstra_path_walk(self._path_c, pointer_x, pointer_y):
289
            path.append((pointer_x[0], pointer_y[0]))
290
        return path
291