pathfinder.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import heapq
  2. import time
  3. t0 = time.time()
  4. class DiscoveryNode(tuple):
  5. def __new__(cls, x, y, cost=0, ancestors=None, matches=None):
  6. n = tuple.__new__(cls, (x, y))
  7. n.cost = cost
  8. n.ancestors = ancestors if ancestors is not None else []
  9. n.matches = matches if matches is not None else []
  10. return n
  11. def __repr__(self):
  12. return f"<{self[0]}, {self[1]}>"
  13. class Queue:
  14. def __init__(self):
  15. self.items = []
  16. def __bool__(self):
  17. return bool(self.items)
  18. def __repr__(self):
  19. return str(self.items)
  20. def values(self):
  21. return (v for _, v in self.items)
  22. def put(self, priority, item):
  23. while priority in [p for p, _ in self.items]:
  24. priority += 1
  25. heapq.heappush(self.items, (priority, item))
  26. def get(self):
  27. return heapq.heappop(self.items)[1]
  28. def get_items(self):
  29. return heapq.heappop(self.items)
  30. def log(*msg):
  31. print("{} - ".format(str(time.time() - t0)[:5]), *msg)
  32. start = (1, 3)
  33. obstacles = [(0, 0), (1, 0), (3, 0), (9, 0), (11, 0), (12, 0), (0, 1), (1, 1), (5, 1), (7, 1), (11, 1), (12, 1), (3, 3), (9, 3), (1, 4), (2, 3)]
  34. neutrals = [(1, 5), (3, 4), (2, 6), (4, 2), (10, 2), (12, 5), (9, 5), (10, 6), (8, 2)]
  35. width = 13
  36. height = 7
  37. def get_neighbors(x, y):
  38. return [(xn, yn) for xn, yn in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if
  39. 0 <= xn < width and 0 <= yn < height]
  40. def can_move_on(pos):
  41. return pos not in obstacles and pos not in neutrals
  42. class ConversionStep:
  43. def __init__(self):
  44. self.pos = None
  45. self.candidates = []
  46. def __repr__(self):
  47. return f"<{self.pos}, c:{self.candidates}>"
  48. class ConversionPath:
  49. def __init__(self):
  50. self.steps = []
  51. def __repr__(self):
  52. return f"<{self.steps}>"
  53. @classmethod
  54. def make_from_discovery_node(cls, node):
  55. nodes = node.ancestors + [node]
  56. path = cls()
  57. found = []
  58. for node in nodes:
  59. step = ConversionStep()
  60. step.pos = tuple(node)
  61. for m in node.matches:
  62. if m in found:
  63. continue
  64. step.candidates.append(m)
  65. found.append(m)
  66. path.steps.append(step)
  67. return path
  68. def path_to_next_candidate(self):
  69. path = []
  70. for step in self.steps:
  71. path.append(step)
  72. if step.candidates:
  73. return path
  74. return None
  75. def discover_multiple(start, key, limit=5):
  76. nodes = Queue()
  77. winners = Queue()
  78. its, break_on = 0, 1000
  79. origin = DiscoveryNode(*start)
  80. abandon_at = 200 # number of paths explored
  81. nodes.put(0, origin)
  82. for n in get_neighbors(*start):
  83. if key(n):
  84. origin.matches.append(n)
  85. while nodes:
  86. its += 1
  87. if its > break_on:
  88. log(f"<!> discovery broke")
  89. break
  90. if len(nodes.items) > abandon_at:
  91. log("<!> get_conversion_path abandoned")
  92. break
  93. current = nodes.get()
  94. neighbors = get_neighbors(*current)
  95. for pos in neighbors:
  96. if not can_move_on(pos):
  97. continue
  98. moving_cost = 1
  99. matches = []
  100. for n in get_neighbors(*pos):
  101. if n not in current.matches and key(n):
  102. matches.append(n)
  103. cost = current.cost + moving_cost
  104. priority = 1000 * cost - (2000 * (len(current.matches) + len(matches)))
  105. priority += 100 * len([a for a in current.ancestors if a == pos]) # décourage de revenir à une case visitée
  106. node = DiscoveryNode(
  107. pos[0],
  108. pos[1],
  109. cost,
  110. current.ancestors + [current],
  111. current.matches + matches
  112. )
  113. if matches:
  114. winners.put(40 * node.cost - 100 * len(node.matches), node)
  115. nodes.put(priority, node)
  116. if len(current.matches) >= limit or its > break_on:
  117. break
  118. # for p, w in winners.items:
  119. # log(f'* {p} - {w.ancestors + [w]} - {w.matches}')
  120. best_node = winners.get()
  121. path = ConversionPath.make_from_discovery_node(best_node)
  122. log(its)
  123. return path
  124. log("start")
  125. res = discover_multiple(start, key=lambda x: x in neutrals, limit=min(4, len(neutrals)))
  126. log(res)