class Pathfinder: '''Path-finder class. Implemets jump-point search algorithm.''' def __init__(self, board): self.size = board.size width, height = board.size self._field = array([[0 if board.get((x,y)).crossable and \ board.get((x,y)).occupied < 2 else -1 for x in range(width)] \ for y in range(height)]) def find(self, fp, orig, dest): orig, dest = orig.get(), dest.get() self.field = self._field.copy() self.field = dilate(self.field, fp.get_morph_kernel()).astype(int) self.field[self.field == 1] = -1 f = self.field if self.acheck(f, dest): dest = self._find_nearest_free(f, dest) self.orig, self.dest = orig, dest self.queue = Queue() self.queue.add(orig) self.sources = self.amake_2d(None, self.size) s = self.sources self.aset(f, self.dest, -3) self.aset(s, self.orig, -2) all_cards = [(0, 1), (1, 0), (-1, 0), (0, -1)] all_diags = [(1, 1), (1, -1), (-1, 1), (-1, -1)] self.checked = [] while not self.queue.is_empty(): current = self.queue.pop() if current in self.checked: continue self.checked += [current] try: for delta in all_cards: if not self._is_bwds(self.aget(s, current), current, delta): self._expand_card(current, delta) for delta in all_diags: if not self._is_bwds(self.aget(s, current), current, delta): self._expand_diag(current, delta) except FoundPath: break nodes = self._find_nodes() return nodes #self._reconstruct(nodes) def _find_nearest_free(self, field, coords): xx, yy = point r = 0 while True: r += 1 for y in range(yy - r, yy + r + 1): for x in range(xx - r, xx + r + 1): if not self.acheck(field, (x, y)): if self.ain_bounds(field, (x, y)): return x, y def _expand_card(self, coords, delta): f = self.field s = self.sources current = coords cost = self.aget(f, coords) delta_x, delta_y = delta while True: cost += 1 cx, cy = current current = cx + delta_x, cy + delta_y cx, cy = current if current == self.dest: self.aset(s, current, coords) raise FoundPath() if self.acheck(f, current): return False prev_cost = self.aget(f, current) if prev_cost < cost and prev_cost != 0: return self.aset(s, current, coords) self.aset(f, current, cost) # Check forced neighbours u, b = self.acheck(f, (cx, cy - 1)), self.acheck(f, (cx, cy + 1)) du = self.acheck(f, (cx + delta_x, cy - 1)) db = self.acheck(f, (cx + delta_x, cy + 1)) l, r = self.acheck(f, (cx - 1, cy)), self.acheck(f, (cx + 1, cy)) dl = self.acheck(f, (cx - 1, cy + delta_y)) dr = self.acheck(f, (cx + 1, cy + delta_y)) if current != coords and current != self.aget(s, coords): if delta_y == 0 and ((u and not du) or (b and not db)): self.queue.add(current) return True if delta_x == 0 and ((l and not dl) or (r and not dr)): self.queue.add(current) return True def _expand_diag(self, coords, delta): f = self.field s = self.sources current = coords cost = self.aget(f, coords) delta_x, delta_y = delta while True: cost += 1.4 cx, cy = current current = cx + delta_x, cy + delta_y cx, cy = current if current == self.dest: self.aset(s, current, coords) raise FoundPath() if self.acheck(f, current): return False prev_cost = self.aget(f, current) if prev_cost < cost and prev_cost != 0: return self.aset(s, current, coords) self.aset(f, current, cost) # Card jumps if self._expand_card(current, (delta_x, 0)): self.queue.add(current) if self._expand_card(current, (0, delta_y)): self.queue.add(current) # Check forced neighbours s1 = self.acheck(f, (cx - delta_x, cy)) s2 = self.acheck(f, (cx, cy - delta_y)) d1 = self.acheck(f, (cx - delta_x, cy + delta_y)) d2 = self.acheck(f, (cx + delta_x, cy - delta_y)) if (s1 and not d1) or (s2 and not d2): self.queue.add(current) def _find_nodes(self): s = self.sources result = [] current = self.dest while current != self.orig: result += [current] current = self.aget(s, current) return result[::-1] def _reconstruct(self, nodes): if nodes == []: return [] result = [] for i in range(len(nodes) - 1): current = nodes[i] _next = nodes[i + 1] result += [current] delta_x, delta_y = self._find_delta(current, _next) while current != _next: cx, cy = current current = cx + delta_x, cy + delta_y result += [current] return result @staticmethod def _find_delta(pta, ptb): xa, ya = pta xb, yb = ptb dx = (1 if xa < xb else -1) if xa != xb else 0 dy = (1 if ya < yb else -1) if ya != yb else 0 return dx, dy @staticmethod def _is_bwds(pta, ptb, delta): '''Check if when moving along delta from pta, ptb is right behind''' if pta == -2: return False # Nothing is backwards. Used for orig pt try: xa, ya = pta except: raise ValueError( 'Could not unpack {}, expected 2-tuple'.format(pta)) xb, yb = ptb delta_x, delta_y = delta dx = (1 if xa < xb else -1) if xa != xb else 0 dy = (1 if ya < yb else -1) if ya != yb else 0 if delta_x == -dx and delta_y == -dy: return True return False ################################ # Array-related helper methods def ain_bounds(self, field, coords): h, w = field.shape x, y = coords return (y >= 0 and y < h and x >= 0 and x < w) def acheck(self, field, coords, value=-1): if not self.ain_bounds(field, coords): return True h, w = field.shape x, y = coords return field[int(y), int(x)] == value def aget(self, field, coords): x, y = coords if not self.ain_bounds(field, coords): return -1 return field[int(y), int(x)] def aset(self, field, coords, value): x, y = coords if not self.ain_bounds(field, coords): return field[int(y), int(x)] = value def amake_2d(self, fill, size): w, h = size return array([[fill for x in range(w)] for y in range(h)])
class TestQueue(unittest.TestCase): def setUp(self) -> None: self.queue = Queue() def test___str__(self) -> None: self.assertEqual(self.queue.__str__(), "[]") self.queue.push(1) self.assertEqual(self.queue.__str__(), "[1]") self.queue.push(2) self.assertEqual(self.queue.__str__(), "[2, 1]") self.queue.push(3) self.assertEqual(self.queue.__str__(), "[3, 2, 1]") def test_push(self) -> None: self.queue.push(1) self.assertEqual(self.queue.__str__(), "[1]") self.queue.push(2) self.assertEqual(self.queue.__str__(), "[2, 1]") def test_pop(self) -> None: self.assertIsNone(self.queue.pop()) self.queue.push(1) self.assertEqual(self.queue.pop(), 1) self.assertIsNone(self.queue.pop()) self.queue.push(1) self.queue.push(2) self.assertEqual(self.queue.pop(), 1) self.assertEqual(self.queue.pop(), 2) self.assertIsNone(self.queue.pop()) def test_peek(self) -> None: self.assertIsNone(self.queue.peek()) self.queue.push(1) self.assertEqual(self.queue.peek(), 1) self.queue.push(2) self.assertEqual(self.queue.peek(), 1) def test_search(self) -> None: self.assertIsNone(self.queue.search(1)) self.queue.push(1) self.assertEqual(self.queue.search(1), 1) self.queue.push(2) self.assertEqual(self.queue.search(1), 1) self.assertEqual(self.queue.search(2), 2) self.assertIsNone(self.queue.search(-1)) def test_is_empty_clear_len(self) -> None: self.assertEqual(len(self.queue), 0) self.queue.clear() self.assertEqual(len(self.queue), 0) self.assertTrue(self.queue.is_empty()) self.queue.push(1) self.assertEqual(len(self.queue), 1) self.assertFalse(self.queue.is_empty()) self.queue.pop() self.assertEqual(len(self.queue), 0) self.assertTrue(self.queue.is_empty()) self.queue.push(1) self.assertEqual(len(self.queue), 1) self.queue.push(2) self.assertEqual(len(self.queue), 2) self.assertFalse(self.queue.is_empty()) self.queue.clear() self.assertTrue(self.queue.is_empty()) self.assertEqual(len(self.queue), 0) def test_big_test(self) -> None: test_case = 100_000 for i in range(test_case): self.queue.push(i) for i in range(test_case): self.assertEqual(self.queue.pop(), i) self.assertIsNone(self.queue.pop()) def test_repr(self) -> None: for i in range(6): self.queue.push(i) self.assertEqual(self.queue.__repr__(), "Queue [5, 4, 3, 2, 1, 0]") self.queue.push(6) self.assertEqual(self.queue.__repr__(), "Queue [6, 5, 4, 3, 2, 1, ...]")