def test_givenEmptyMazeAndRobotRadius_whenAddObstacle_thenRobotRadiusIsTakenIntoAccountWhenAddingObstacle(
        self, ):
        expected_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))
        obstacle_position = Position(13, 9)
        actual_maze = self.maze_factory.create_from_shape((17, 20, 0))

        actual_maze.add_obstacle(obstacle_position, self.A_ROBOT_RADIUS,
                                 self.AN_OBSTACLE_RADIUS)

        self.assertEqual(expected_maze, actual_maze)
    def create_from_shape(self, shape: Tuple[int, int, int]) -> Maze:
        width, height, *_ = shape
        array = np.zeros((width, height))
        for i in range(width):
            for j in range(height):
                if i == 0 or j == 0 or i == width - 1 or j == height - 1:
                    array[i][j] = 1

        return Maze(array)
    def test_givenPuckObstacle_whenRemovePuckAsObstacle_thenObstacleIsRemove(
            self):
        expected_maze = self.maze_factory.create_from_shape((8, 10, 0))
        actual_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))

        actual_maze.remove_puck_as_obstacle(Position(5, 4), obstacle_radius=2)

        self.assertEqual(actual_maze, expected_maze)
    def test_whenCreateFromShape_thenMazeHasBorderOfOnesAndRightShape(self, ):
        maze_width = 5
        maze_height = 10
        shape = (maze_width, maze_height, 0)
        expected_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))

        maze = self.maze_factory.create_from_shape(shape)

        self.assertEqual(maze, expected_maze)
    def test_givenObstacleCloseToEdge_whenAddObstacle_thenDontRaiseIndexOufOfBoundsException(
        self, ):
        expected_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))

        obstacle_position = Position(6, 8)
        actual_maze = self.maze_factory.create_from_shape((8, 10, 0))
        actual_maze.add_obstacle(obstacle_position, self.A_ROBOT_RADIUS,
                                 self.AN_OBSTACLE_RADIUS)

        self.assertEqual(expected_maze, actual_maze)
 def _validate_maze(self, maze: Maze) -> Maze:
     if maze.get_shape() != (0, ) and len(maze.get_shape()) <= 2:
         return maze
     raise InvalidMazeException
class TestAStarShortestPathAlgorithm(TestCase):
    A_BAD_MAZE = Maze(array=np.array([]))
    A_MAZE = Maze(array=np.array([
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    ]))
    AN_HORIZONTALLY_OUT_OF_BOUND_POSITION = Position(1, 25)
    A_VERTICALLY_OUT_OF_BOUND_POSITION = Position(12, 1)
    A_NEGATIVE_HORIZONTAL_POSITION = Position(1, -1)
    A_NEGATIVE_VERTICAL_POSITION = Position(-1, 1)
    A_VALID_POSITION = Position(1, 1)
    ANOTHER_VALID_POSITION = Position(2, 2)
    AN_OBSTACLE_POSITION = Position(4, 0)

    def setUp(self) -> None:
        self.a_start_algorithm = AStarShortestPathAlgorithm()
        self.a_start_algorithm.set_maze(self.A_MAZE)

    def test_givenABadMaze_whenInitializingAlgorithm_thenRaiseInvalidMazeException(
        self, ):
        algorithm = AStarShortestPathAlgorithm()

        with self.assertRaises(InvalidMazeException):
            algorithm.set_maze(self.A_BAD_MAZE)

    def test_givenNoneMazeSet_whenFindPath_thenRaiseInvalidMazeException(self):
        algorithm = AStarShortestPathAlgorithm()

        with self.assertRaises(InvalidMazeException):
            algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_VALID_POSITION, self.ANOTHER_VALID_POSITION)

    def test_givenStartPositionWithNegativeCoordinate_whenFindPath_thenRaiseInvalidStartPointException(
        self, ):
        with self.assertRaises(InvalidStartPointException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_NEGATIVE_VERTICAL_POSITION, self.A_VALID_POSITION)

    def test_givenHorizontallyOutOfBoundsStartPosition_whenFindPath_thenRaiseInvalidStartPointException(
        self, ):
        with self.assertRaises(InvalidStartPointException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.AN_HORIZONTALLY_OUT_OF_BOUND_POSITION,
                self.A_VALID_POSITION)

    def test_givenVerticallyOutOfBoundsStartPosition_whenFindPath_thenRaiseInvalidStartPointException(
        self, ):
        with self.assertRaises(InvalidStartPointException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_VERTICALLY_OUT_OF_BOUND_POSITION, self.A_VALID_POSITION)

    def test_givenEndPositionWithNegativeCoordinate_whenFindPath_thenRaiseInvalidStartPointException(
        self, ):
        with self.assertRaises(InvalidStartPointException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_NEGATIVE_HORIZONTAL_POSITION, self.A_VALID_POSITION)

    def test_givenHorizontallyOutOfBoundsEndingPosition_whenFindPath_thenRaiseInvalidDestinationException(
        self, ):
        with self.assertRaises(InvalidDestinationException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_VALID_POSITION,
                self.AN_HORIZONTALLY_OUT_OF_BOUND_POSITION)

    def test_givenVerticallyOutOfBoundsEndingPosition_whenFindPath_thenRaiseInvalidDestinationException(
        self, ):
        with self.assertRaises(InvalidDestinationException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_VALID_POSITION, self.A_VERTICALLY_OUT_OF_BOUND_POSITION)

    def test_givenAMaze_whenFindPathWithObstacleAsDestination_thenRaiseInvalidDestinationException(
        self, ):
        with self.assertRaises(InvalidDestinationException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.A_VALID_POSITION, self.AN_OBSTACLE_POSITION)

    def test_givenAMaze_whenFindPathWithObstacleAsStartPoint_thenRaiseInvalidDestinationException(
        self, ):
        with self.assertRaises(InvalidStartPointException):
            self.a_start_algorithm.find_shortest_path_with_cartesian_coordinates(
                self.AN_OBSTACLE_POSITION, self.A_VALID_POSITION)

    def test_givenStartAndEndPosition_whenFindPathWithCartesianCoordinates_thenFlipsCoordinatesAndFindsPath(
        self, ):
        starting_position = Position(2, 1)
        ending_position = Position(5, 1)
        expected_path = Path([
            Position(2, 1),
            Position(3, 1),
            Position(3, 2),
            Position(3, 3),
            Position(3, 4),
            Position(3, 5),
            Position(3, 6),
            Position(4, 6),
            Position(5, 6),
            Position(5, 5),
            Position(5, 4),
            Position(5, 3),
            Position(5, 2),
            Position(5, 1),
        ])

        actual_path = (self.a_start_algorithm.
                       find_shortest_path_with_cartesian_coordinates(
                           starting_position, ending_position))

        self.assertEqual(expected_path, actual_path)
class TestMaze(TestCase):
    A_ROBOT_RADIUS = 2
    AN_OBSTACLE_RADIUS = 3

    A_MAZE_ARRAY = np.array([
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
    ])
    AN_INDEX = 2

    def setUp(self) -> None:
        self.maze_factory = MazeFactory(self.A_ROBOT_RADIUS,
                                        self.AN_OBSTACLE_RADIUS)
        self.maze = Maze(self.A_MAZE_ARRAY)

    def test_whenGetShape_thenReturnNumpyShape(self) -> None:
        expected_shape = self.A_MAZE_ARRAY.shape

        actual_shape = self.maze.get_shape()

        self.assertEqual(expected_shape, actual_shape)

    def test_whenGetItem_thenReturnItemInNumpyArray(self) -> None:
        expected_item = self.A_MAZE_ARRAY[self.AN_INDEX][self.AN_INDEX]

        actual_item = self.maze[self.AN_INDEX][self.AN_INDEX]

        self.assertEqual(expected_item, actual_item)

    def test_givenEmptyMazeAndRobotRadius_whenAddObstacle_thenRobotRadiusIsTakenIntoAccountWhenAddingObstacle(
        self, ):
        expected_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))
        obstacle_position = Position(13, 9)
        actual_maze = self.maze_factory.create_from_shape((17, 20, 0))

        actual_maze.add_obstacle(obstacle_position, self.A_ROBOT_RADIUS,
                                 self.AN_OBSTACLE_RADIUS)

        self.assertEqual(expected_maze, actual_maze)

    def test_givenObstacleCloseToEdge_whenAddObstacle_thenDontRaiseIndexOufOfBoundsException(
        self, ):
        expected_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))

        obstacle_position = Position(6, 8)
        actual_maze = self.maze_factory.create_from_shape((8, 10, 0))
        actual_maze.add_obstacle(obstacle_position, self.A_ROBOT_RADIUS,
                                 self.AN_OBSTACLE_RADIUS)

        self.assertEqual(expected_maze, actual_maze)

    def test_givenPuckObstacle_whenRemovePuckAsObstacle_thenObstacleIsRemove(
            self):
        expected_maze = self.maze_factory.create_from_shape((8, 10, 0))
        actual_maze = Maze(
            np.array([
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 0, 0, 1, 1, 1, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ]))

        actual_maze.remove_puck_as_obstacle(Position(5, 4), obstacle_radius=2)

        self.assertEqual(actual_maze, expected_maze)
 def setUp(self) -> None:
     self.maze_factory = MazeFactory(self.A_ROBOT_RADIUS,
                                     self.AN_OBSTACLE_RADIUS)
     self.maze = Maze(self.A_MAZE_ARRAY)