def diagonal_forced_neighbors(coord, direction): forced_candidates = {Coord(coord.x, coord.y - direction.y)} forced = set( filter(lambda candidate: candidate in self.grid.obstacles(), forced_candidates)) return set( map(lambda cell: Coord(cell.x + direction.x, cell.y), forced))
def test_start_successors(self): """ Test that a starting node produces all neighbors of it as successors. """ expected = {JPSNode(Coord(0, 3), Coord(0, 1))} actual = self.jps.successors(JPSNode(Coord(0, 0), None), Coord(0, 3)) self.assertEqual(expected, actual)
def test_diagonal_execute(self): goal = Coord(3, 3) expected = [Coord(0, 0), goal] path = list( map(lambda jump_point: jump_point.coord, self.jps.execute((Coord(0, 0), goal)))) self.assertEqual(expected, path)
def test_forced_diagonal(self): obstacle_grid = DiagonalGrid(4, 4, [Coord(2, 1)]) diag_jps = JumpPointSearch(obstacle_grid, diagonal) expected = Coord(3, 1) current = JPSNode(Coord(2, 2), Coord(1, 1)) neighbors = set( diag_jps.forced_neighbors(current.coord, current.direction)) self.assertTrue(expected in neighbors)
def test_tricky_path(self): tricky_grid = OrthogonalGrid(3, 3, [Coord(0, 1), Coord(2, 1)]) path = set( AStar(tricky_grid, manhattan).execute((Coord(0, 0), Coord(2, 2)))) expected = { Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(1, 2), Coord(2, 2) } coord_path = set(map(lambda cell: Coord(cell.x, cell.y), path)) self.assertEqual(expected, coord_path)
def test_obstacle_execute(self): expected = [ JPSNode(Coord(0, 0), None), JPSNode(Coord(1, 1), Coord(1, 1)), JPSNode(Coord(1, 2), Coord(0, 1)), JPSNode(Coord(0, 3), Coord(-1, 1)) ] obstacle_jps = JumpPointSearch(self.obstacle_grid, diagonal) path = obstacle_jps.execute((Coord(0, 0), Coord(0, 3))) self.assertEqual(expected, path, coordinate_mismatch_message(expected, path))
def straight_forced_neighbors(coord, direction): orthogonal_direction = Coord(direction.y, direction.x) forced_candidates = { Coord(coord.x + orthogonal_direction.x, coord.y + orthogonal_direction.y), Coord(coord.x - orthogonal_direction.x, coord.y - orthogonal_direction.y) } forced = set( filter(lambda candidate: candidate in self.grid.obstacles(), forced_candidates)) return set( map( lambda cell: Coord(cell.x + direction.x, cell.y + direction .y), forced))
def connect_jump_points(begin, end): total_cells = int(self.heuristic_fn(begin.coord, end.coord)) return list( map( lambda offset: Coord( begin.coord.x + end.direction.x * offset, begin.coord.y + end.direction.y * offset), range(0, total_cells)))
def jump(self, parent: Coord, direction: Coord, goal): """ Finds next jump point recursively. Base cases: 1. Next coordinate is out of bounds 2. Next coordinate is an obstacle 3. Next coordinate has forced neighbors :param parent: Previously considered point (not necessarily a jump point) :param direction: Direction to try finding next jump point. :param goal: Goal cell. :return: Next jump point to consider or None if direction is invalid. """ def straight_moves(): return {Coord(0, direction.y), Coord(direction.x, 0)} def invalid_direction() -> bool: return not self.grid.is_valid_coord( next_coord) or next_coord in self.grid.obstacles() def is_jump_point() -> bool: return next_coord == goal or self.has_forced_neighbors( next_coord, direction) next_coord = Coord(parent.x + direction.x, parent.y + direction.y) if invalid_direction(): return None if is_jump_point(): return next_coord if self.is_diagonal(direction): for i in straight_moves(): if self.jump(next_coord, i, goal) is not None: return next_coord return self.jump(next_coord, direction, goal)
def natural_neighbors(self, cell: Coord, direction: Coord) -> {}: """ :param cell: A cell inside the current grid. :param direction: Direction previously traveled. :return: A set containing any natural neighbors of a cell. """ if self.is_diagonal(direction): natural_neighbors = { Coord(cell.x + direction.x, cell.y), Coord(cell.x, cell.y + direction.y), Coord(cell.x + direction.x, cell.y + direction.y) } else: natural_neighbors = { Coord(cell.x + direction.x, cell.y + direction.y) } return natural_neighbors
def test_neighbors(self): coord = Coord(1, 1) neighbors = set(self.grid.neighbors(coord)) self.assertEqual( { Coord(0, 0), Coord(0, 1), Coord(1, 0), Coord(2, 1), Coord(2, 2), Coord(0, 2), Coord(2, 0), Coord(1, 2) }, neighbors)
def neighbors(self, coord: Coord) -> list: """ :param coord: :return: All neighbors of coord in a list. (A coord with no neighbors would return empty list) """ make_neighbor = lambda x, y: Coord(coord.x + x, coord.y + y) dist = map(lambda i: make_neighbor(i[0], i[1]), [(0, -1), (0, 1), (-1, 0), (1, 0)]) return list(filter(lambda i: self.is_adjacent(coord, i), dist))
def test_diagonal_obstacle(self): expected = [ JPSNode(Coord(0, 0), None), JPSNode(Coord(2, 2), Coord(1, 1)), JPSNode(Coord(3, 3), Coord(1, 1)) ] diagonal_obstacle_grid = DiagonalGrid(4, 4, [Coord(2, 1)]) obstacle_jps = JumpPointSearch(diagonal_obstacle_grid, diagonal) path = obstacle_jps.execute((Coord(0, 0), Coord(3, 3))) self.assertEqual(expected, path, coordinate_mismatch_message(expected, path))
def __init__(self): self.straight_coordinates = { Directions.TOP: Coord(-1, 0), Directions.BOTTOM: Coord(1, 0), Directions.LEFT: Coord(0, -1), Directions.RIGHT: Coord(0, 1), } self.diagonal_coordinates = { Directions.TOP_LEFT: Coord(-1, -1), Directions.TOP_RIGHT: Coord(1, 1), Directions.BOTTOM_LEFT: Coord(-1, 1), Directions.BOTTOM_RIGHT: Coord(-1, 1) }
def successors(self, current: JPSNode, goal: Coord): """ Finds jump point successors of current JPSNode. :param current: :param goal: :return: A set of successors, including empty set if no successors are found. """ succ = set() neighbors = self.prune(current) parent_x, parent_y = current.coord.x, current.coord.y for neighbor in neighbors: dir_coord = self.direction(current.coord, neighbor) next_jump_point = self.jump(Coord(parent_x, parent_y), Coord(dir_coord.x, dir_coord.y), goal) if next_jump_point is not None: next_node = JPSNode(next_jump_point, dir_coord) next_node.g = current.g + STRAIGHT_COST next_node.f = next_node.g + self.heuristic_fn( next_node.coord, goal) succ.add(next_node) return succ
def test_large_obstacle_grid(self): large_grid = DiagonalGrid( 10, 10, [Coord(1, 8), Coord(5, 7), Coord(6, 0), Coord(7, 7)]) obstacle_jps = JumpPointSearch(large_grid, diagonal_tie_breaker) path = obstacle_jps.execute((Coord(0, 0), Coord(9, 9)))
def print_grid(grid: OrthogonalGrid, path: []): """ Print a grid with path. :param grid: OrthogonalGrid to print :param path: Path """ for i in range(0, grid.xsize): for j in range(0, grid.ysize): coord = Coord(i, j) if coord in path: val = 'P' elif coord in grid.obstacles(): val = 'X' else: val = '.' print(val, sep=' ', end=' ') print()
def test_only_path(self): """ Only one path goes through this three by three grid. test_path method should give us back this one path. It looks like: |X|X|E| | | | | |S|X|X| """ algo = AStar(self.grid, manhattan) path = set(algo.execute((Coord(0, 0), Coord(2, 2)))) expected = { Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(1, 2), Coord(2, 2) } coord_path = set(map(lambda cell: Coord(cell.x, cell.y), path)) self.assertEqual(expected, coord_path)
def test_prune_start_node(self): expected = {Coord(0, 1)} current = JPSNode(Coord(0, 0), Coord(0, 1)) neighbors = set(self.jps.prune(current)) self.assertEqual(expected, neighbors)
def test_not_forced_neighbor(self): forced = self.jps.has_forced_neighbors(Coord(0, 0), Coord(0, 1)) self.assertFalse( forced, "Error: forced neighbor occurred in empty grid space.")
def test_is_forced_neighbor(self): forced_neighbor_grid = DiagonalGrid(4, 4, [Coord(1, 2)]) obstacle_jps = JumpPointSearch(forced_neighbor_grid, diagonal) neighbors = obstacle_jps.has_forced_neighbors(Coord(0, 2), Coord(0, 1)) self.assertTrue(neighbors)
def test_blocked_jump(self): obstacle_jps = JumpPointSearch(self.obstacle_grid, diagonal) coord = obstacle_jps.jump(Coord(0, 1), Coord(0, 1), Coord(0, 3)) self.assertIsNone(coord)
def test_diagonal_jump(self): expected_coord = Coord(3, 3) coord = self.jps.jump(Coord(0, 0), Coord(1, 1), expected_coord) self.assertEqual(expected_coord, coord)
def test_obstacle_execute(self): obstacle_grid = DiagonalGrid(4, 4, [Coord(0, 2)]) expected = [Coord(0, 0), Coord(0, 1), Coord(1, 0)]
def test_full_diagonal_path(self): expected = [Coord(0, 0), Coord(1, 1), Coord(2, 2), Coord(3, 3)] path = self.jps.execute((Coord(0, 0), Coord(3, 3))) self.assertEqual(expected, self.jps.connect_path(path))
def test_full_horizontal_path(self): goal = Coord(0, 3) expected = [Coord(0, 0), Coord(0, 1), Coord(0, 2), Coord(0, 3)] path = self.jps.execute((Coord(0, 0), goal)) self.assertEqual(expected, self.jps.connect_path(path))
def test_prune_horizontal_node(self): expected = {Coord(1, 2)} current = JPSNode(Coord(1, 1), Coord(0, 1)) neighbors = set(self.jps.prune(current)) self.assertEqual(expected, neighbors)
def test_horizontal_jump(self): expected_coord = Coord(0, 3) coord = self.jps.jump(Coord(0, 0), Coord(0, 1), expected_coord) self.assertEqual(expected_coord, coord)
def setUp(self): self.grid = DiagonalGrid(4, 4, []) self.obstacle_grid = DiagonalGrid(4, 4, [Coord(0, 2)]) self.jps = JumpPointSearch(self.grid, diagonal)
def test_horizontal_execute(self): goal = Coord(0, 3) expected = [JPSNode(Coord(0, 0), None), JPSNode(goal, Coord(0, 1))] path = self.jps.execute((Coord(0, 0), goal)) self.assertEqual(expected, path)