grid.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # -*- coding: utf-8 -*-
  2. from .node import Node
  3. try:
  4. import numpy as np
  5. USE_NUMPY = True
  6. except ImportError:
  7. USE_NUMPY = False
  8. from pathfinding.core.diagonal_movement import DiagonalMovement
  9. def build_nodes(width, height, matrix=None, inverse=False):
  10. """
  11. create nodes according to grid size. If a matrix is given it
  12. will be used to determine what nodes are walkable.
  13. :rtype : list
  14. """
  15. nodes = []
  16. use_matrix = (isinstance(matrix, (tuple, list))) or \
  17. (USE_NUMPY and isinstance(matrix, np.ndarray) and matrix.size > 0)
  18. for y in range(height):
  19. nodes.append([])
  20. for x in range(width):
  21. # 1, '1', True will be walkable
  22. # while others will be obstacles
  23. # if inverse is False, otherwise
  24. # it changes
  25. weight = int(matrix[y][x]) if use_matrix else 1
  26. walkable = weight <= 0 if inverse else weight >= 1
  27. nodes[y].append(Node(x=x, y=y, walkable=walkable, weight=weight))
  28. return nodes
  29. class Grid(object):
  30. def __init__(self, width=0, height=0, matrix=None, inverse=False):
  31. """
  32. a grid represents the map (as 2d-list of nodes).
  33. """
  34. self.width = width
  35. self.height = height
  36. if isinstance(matrix, (tuple, list)) or (
  37. USE_NUMPY and isinstance(matrix, np.ndarray) and
  38. matrix.size > 0):
  39. self.height = len(matrix)
  40. self.width = self.width = len(matrix[0]) if self.height > 0 else 0
  41. if self.width > 0 and self.height > 0:
  42. self.nodes = build_nodes(self.width, self.height, matrix, inverse)
  43. else:
  44. self.nodes = [[]]
  45. def node(self, x, y):
  46. """
  47. get node at position
  48. :param x: x pos
  49. :param y: y pos
  50. :return:
  51. """
  52. return self.nodes[y][x]
  53. def inside(self, x, y):
  54. """
  55. check, if field position is inside map
  56. :param x: x pos
  57. :param y: y pos
  58. :return:
  59. """
  60. return 0 <= x < self.width and 0 <= y < self.height
  61. def walkable(self, x, y):
  62. """
  63. check, if the tile is inside grid and if it is set as walkable
  64. """
  65. return self.inside(x, y) and self.nodes[y][x].walkable
  66. def neighbors(self, node, diagonal_movement=DiagonalMovement.never):
  67. """
  68. get all neighbors of one node
  69. :param node: node
  70. """
  71. x = node.x
  72. y = node.y
  73. neighbors = []
  74. s0 = d0 = s1 = d1 = s2 = d2 = s3 = d3 = False
  75. # ↑
  76. if self.walkable(x, y - 1):
  77. neighbors.append(self.nodes[y - 1][x])
  78. s0 = True
  79. # →
  80. if self.walkable(x + 1, y):
  81. neighbors.append(self.nodes[y][x + 1])
  82. s1 = True
  83. # ↓
  84. if self.walkable(x, y + 1):
  85. neighbors.append(self.nodes[y + 1][x])
  86. s2 = True
  87. # ←
  88. if self.walkable(x - 1, y):
  89. neighbors.append(self.nodes[y][x - 1])
  90. s3 = True
  91. if diagonal_movement == DiagonalMovement.never:
  92. return neighbors
  93. if diagonal_movement == DiagonalMovement.only_when_no_obstacle:
  94. d0 = s3 and s0
  95. d1 = s0 and s1
  96. d2 = s1 and s2
  97. d3 = s2 and s3
  98. elif diagonal_movement == DiagonalMovement.if_at_most_one_obstacle:
  99. d0 = s3 or s0
  100. d1 = s0 or s1
  101. d2 = s1 or s2
  102. d3 = s2 or s3
  103. elif diagonal_movement == DiagonalMovement.always:
  104. d0 = d1 = d2 = d3 = True
  105. # ↖
  106. if d0 and self.walkable(x - 1, y - 1):
  107. neighbors.append(self.nodes[y - 1][x - 1])
  108. # ↗
  109. if d1 and self.walkable(x + 1, y - 1):
  110. neighbors.append(self.nodes[y - 1][x + 1])
  111. # ↘
  112. if d2 and self.walkable(x + 1, y + 1):
  113. neighbors.append(self.nodes[y + 1][x + 1])
  114. # ↙
  115. if d3 and self.walkable(x - 1, y + 1):
  116. neighbors.append(self.nodes[y + 1][x - 1])
  117. return neighbors
  118. def cleanup(self):
  119. for y_nodes in self.nodes:
  120. for node in y_nodes:
  121. node.cleanup()
  122. def grid_str(self, path=None, start=None, end=None,
  123. border=True, start_chr='s', end_chr='e',
  124. path_chr='x', empty_chr=' ', block_chr='#',
  125. show_weight=False):
  126. """
  127. create a printable string from the grid using ASCII characters
  128. :param path: list of nodes that show the path
  129. :param start: start node
  130. :param end: end node
  131. :param border: create a border around the grid
  132. :param start_chr: character for the start (default "s")
  133. :param end_chr: character for the destination (default "e")
  134. :param path_chr: character to show the path (default "x")
  135. :param empty_chr: character for empty fields (default " ")
  136. :param block_chr: character for blocking elements (default "#")
  137. :param show_weight: instead of empty_chr show the cost of each empty
  138. field (shows a + if the value of weight is > 10)
  139. :return:
  140. """
  141. data = ''
  142. if border:
  143. data = '+{}+'.format('-'*len(self.nodes[0]))
  144. for y in range(len(self.nodes)):
  145. line = ''
  146. for x in range(len(self.nodes[y])):
  147. node = self.nodes[y][x]
  148. if node == start:
  149. line += start_chr
  150. elif node == end:
  151. line += end_chr
  152. elif path and ((node.x, node.y) in path or node in path):
  153. line += path_chr
  154. elif node.walkable:
  155. # empty field
  156. weight = str(node.weight) if node.weight < 10 else '+'
  157. line += weight if show_weight else empty_chr
  158. else:
  159. line += block_chr # blocked field
  160. if border:
  161. line = '|'+line+'|'
  162. if data:
  163. data += '\n'
  164. data += line
  165. if border:
  166. data += '\n+{}+'.format('-'*len(self.nodes[0]))
  167. return data