rover.py 4.99 KB
import logging


class OutOfBoundaryException(Exception):
    """
    Raised when a Rover gets starting coordinates that are not on the planet
    """


class InvalidHeading(Exception):
    """
    Raised when a heading that does not exist is provided
    """


class InvalidTurn(Exception):
    """
    Raised when a turning direction that does not exist is provided
    """


class MoveOutOfBounds(Exception):
    """
    Raised when an instruction would cause a Rover to exceed the planetary
    boundaries
    """


class Rover(object):
    """
    This Mars Rover should keep track of its current position, its heading, and
    the size of the grid
    """
    def __init__(self, start, route, boundaries):
        self.boundaries = [[0, 0], [0, 0]]
        self.coordinates = [0, 0]
        self.heading = ''
        self.headings = ['N', 'E', 'S', 'W']
        self.valid_instructions = ['L', 'R', 'M']
        self.moves = {
            'N': self.move_north,
            'S': self.move_south,
            'E': self.move_east,
            'W': self.move_west
        }
        self.turns = {
            'L': {'N': 'W', 'W': 'S', 'S': 'E', 'E': 'N'},
            'R': {'N': 'E', 'E': 'S', 'S': 'W', 'W': 'N'}
        }

        self.set_boundaries(boundaries)

        s = start.split()
        self.set_coordinates(s)
        self.set_heading(s[2])

        self.route = route

    def set_boundaries(self, b):
        """Define the boundaries of the planet"""
        b = b.split()
        # providing a non-integer string will raise a value error, which should
        # be fine for now
        self.boundaries[1][0] = int(b[0])
        self.boundaries[1][1] = int(b[1])

    def set_coordinates(self, coords):
        """Set the current coordinates for the rover"""
        # providing a non-integer string will raise a value error, which should
        # be fine for now
        x = int(coords[0])
        y = int(coords[1])

        # check if x and y can actually exist within the planetary boundaries
        if x < self.min_x or x > self.max_x or y < self.min_y or y > self.max_y:
            raise OutOfBoundaryException

        self.coordinates = [x, y]

    def set_heading(self, h):
        """Set the current heading of the rover"""
        if h not in self.headings:
            raise InvalidHeading('Heading "{}" is not allowed'.format(h))
        self.heading = h

    def execute_route(self, route):
        for instruction in route:
            self.step(instruction)

    def step(self, instruction):
        if instruction not in self.valid_instructions:
            # If the rover encounters an invalid instruction, we don't want it
            # to crash and burn. For now, we simply log that an invalid
            # instruction was received, and return
            # note to self: we may want to do that in the set methods as well
            return

        if instruction == 'M':
            # move forward; if the result of the action would me to fall off
            # the planet, it will simply sit at the edge until a more sensible
            # instruction is sent, and log a message to a attentive sysadmin;
            # we don't want it to get stuck
            try:
                self.move()
            except MoveOutOfBounds as e:
                position = '{} {} {}'.format(self.coordinates[0],
                                             self.coordinates[1],
                                             self.heading)
                logging.info('Rover cannot go farther {}; it will stay on {}'
                             .format(position, self.heading))
        else:
            # we are turning
            self.turn(instruction)

    def move(self):
        # move in the current heading
        self.moves[self.heading]()

    def turn(self, direction):
        # set the new heading based on the instructed direction and current
        # heading
        if direction not in self.turns:
            raise InvalidTurn('Direction "{}" does not exist')
        self.set_heading(self.turns[direction][self.heading])

    def move_north(self):
        if self.coordinates[1] + 1 > self.max_y:
            raise MoveOutOfBounds('Cannot go further north')
        self.coordinates[1] += 1

    def move_south(self):
        if self.coordinates[1] - 1 < self.min_y:
            raise MoveOutOfBounds('Cannot go further south')
        self.coordinates[1] -= 1

    def move_east(self):
        if self.coordinates[0] + 1 > self.max_x:
            raise MoveOutOfBounds('Cannot go further east')
        self.coordinates[0] += 1

    def move_west(self):
        if self.coordinates[0] - 1 < self.min_x:
            raise MoveOutOfBounds('Cannot go further west')
        self.coordinates[0] -= 1

    # some convenient properties to access the boundaries lists
    @property
    def min_x(self):
        return self.boundaries[0][0]

    @property
    def min_y(self):
        return self.boundaries[0][1]

    @property
    def max_x(self):
        return self.boundaries[1][0]

    @property
    def max_y(self):
        return self.boundaries[1][1]