From e41e360ed8d29b07f7576cd6b1f2bc70dc73a66a Mon Sep 17 00:00:00 2001 From: Chris Davoren Date: Wed, 27 Sep 2023 09:07:09 +1000 Subject: [PATCH] Initial commit of files. --- .gitignore | 1 + toyrobot/__init__.py | 3 + toyrobot/robot.py | 243 +++++++++++++++++++++++++++++++++++++++++++ trmain.py | 22 ++++ 4 files changed, 269 insertions(+) create mode 100644 .gitignore create mode 100644 toyrobot/__init__.py create mode 100644 toyrobot/robot.py create mode 100644 trmain.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/toyrobot/__init__.py b/toyrobot/__init__.py new file mode 100644 index 0000000..3a54583 --- /dev/null +++ b/toyrobot/__init__.py @@ -0,0 +1,3 @@ +# __init__.py + +from .robot import Robot diff --git a/toyrobot/robot.py b/toyrobot/robot.py new file mode 100644 index 0000000..148ad15 --- /dev/null +++ b/toyrobot/robot.py @@ -0,0 +1,243 @@ +class Robot: + """ + Class representing a single "Toy Robot". Robots instances have knowledge of their + position, direction, and movement limits. + + Attributes: + DEFAULT_MAX_X (int): The default maximum allowable horizontal coordinate + (inclusive). + DEFAULT_MAX_Y (int): The default maximum allowable vertical coordinate + (inclusive). + DIRECTIONS (dict): A dictionary (str: int) of direction names (e.g. NORTH, EAST + etc) and their numerical encoding. Only the keys are intended for use externally as a list of valid directions. + """ + + DEFAULT_MAX_X = 5 + DEFAULT_MAX_Y = 5 + + # Directions ordered such that adding 1 corresponds to RIGHT turn + DIRECTIONS = { + "NORTH": 0, + "EAST": 1, + "SOUTH": 2, + "WEST": 3, + } + + # Private internals + # Key corresponds to numerical direction defined in Robot.directions + # Value is an offset vector in with x and y keys (positive x is NORTH, positive y is + # EAST) + _MOVEMENT_VECTORS = { + 0: {"x": 0, "y": 1}, + 1: {"x": 1, "y": 0}, + 2: {"x": 0, "y": -1}, + 3: {"x": -1, "y": 0}, + } + + def __init__(self, max_x: int = DEFAULT_MAX_X, max_y: int = DEFAULT_MAX_Y): + """ + Creates a new Robot instance with the specified position limits (optional). + Implicitly, the minimum limit on coordinates is 0 both horizontally and + vertically. + + See the class attributes `DEFAULT_MAX_X` and `DEFAULT_MAX_Y` for the respective + default numerical values. + + Args: + max_x (int): The maximum allowable horizontal coordinate (inclusive, + positive is EAST). + max_y (int): The maximum allowable vertical coordinate (inclusive, + positive is NORTH). + + Raises: + ValueError: If max_x or max_y are less than 0. + """ + + if max_x < 0 or max_y < 0: + raise ValueError("Cannot specify negative limits") + + self._max_x = max_x + self._max_y = max_y + + self._position_x = None + self._position_y = None + self._direction = None + + def set_limits(self, max_x: int, max_y: int): + """ + Sets the positional limits for this Robot. See __init__() for details. + + Args: + max_x (int): New maximum allowable horizontal coordinate. + max_y (int): New maximum allowable vertical coordinate. + + Raises: + ValueError: If max_x or max_y are less than 0. + """ + + if max_x < 0 or max_y < 0: + raise ValueError("Cannot specify negative limits") + + self._max_x = max_x + self._max_y = max_y + + def get_limits(self) -> (int, int): + """ + Returns the current maximum coordinates valid for this instance. + + Returns: + (int, int): A tuple of the maximum coordinates in the order (maximum_x, + maximum y). + """ + return (self._max_x, self._max_y) + + def valid_position(self, x: int, y: int) -> bool: + """ + Calculates whether the given coordinates are valid for the limits set on this + Robot instance. + + This function is used by the `move()` function to verify that a move action + would be successful. + + Args: + x: Proposed horizontal coordinate. + y: Proposed vertical coordinate. + + Returns: + bool: True if given coordinates are within limits, otherwise False. + """ + + return x >= 0 and y >= 0 and x <= self._max_x and y <= self._max_y + + def is_initialized(self): + """ + Returns whether this Robot instance has been initialized (i.e. whether a valid + `place()` command has been executed). + + This function is used by the `move()` function to verify that the instance has + been correctly placed before movement. + + Returns: + bool: True if correctly initialized, otherwise false. + """ + return ( + self._position_x is not None + and self._position_y is not None + and self._direction is not None + ) + + def move(self): + """ + Moves the robot one space in the direction it is currently facing, provided that + said movement would be within limits. + + Will do nothing if this Robot instance has not been initialized successfully + with `place()` or the specified movement would be out of bounds. + """ + + if not self.is_initialized(): + return + + vector = Robot._MOVEMENT_VECTORS[self._direction] + new_position_x = self._position_x + vector["x"] + new_position_y = self._position_y + vector["y"] + + if not Robot.valid_position(new_position_x, new_position_y): + return + + self._position_x = new_position_x + self._position_y = new_position_y + + def place(self, position_x: int, position_y: int, direction_name: str): + """ + Places the Robot instance at the specified coordinates with the specified + direction. + + Will do nothing if the given coordinates are out of bounds, or the direction is + not one of the keys in `Robot.DIRECTIONS`. + + See `__init__()` for detailed coordinate information. + + Args: + position_x (int): Horizontal coordinate for placement. + position_y (int): Vertical coordinate for placement. + direction_name (str): Direction of placement; must be one of the string keys + in `Robot.DIRECTIONS`. + """ + direction_name = direction_name.upper() + if direction_name not in Robot.DIRECTIONS.keys() or not self.valid_position( + position_x, position_y + ): + return + + self._position_x = position_x + self._position_y = position_y + self._direction = Robot.DIRECTIONS[direction_name] + + def get_position(self) -> (int, int): + """ + Returns the current position of this Robot instance. + + Returns: + (int, int): A tuple with this instance's current coordinates in the order + (position_x, position_y), or (None, None) if this instance has not yet + been initialized. + """ + return ( + (self._position_x, self._position_y) + if self.is_initialized() + else (None, None) + ) + + def get_direction(self) -> str: + """ + Returns the direction in which this instance is currently facing in string form. + + Returns: + str: The current direction of this instance; will be one of the keys of + `Robot.DIRECTION`. Will return None if this instance has not yet been + initialized. + """ + return ( + {v: k for k, v in Robot.DIRECTIONS.items()}[self._direction] + if self.is_initialized() + else None + ) + + def rotate_left(self): + """ + Rotates this Robot instance's direction to the LEFT. For example, if currently + facing NORTH, the new direction will be WEST. + + Does nothing if this instance has not yet been correctly placed (initialized). + """ + if not self.is_initialized(): + return + self._direction = (self._direction - 1) % 4 + + def rotate_right(self): + """ + Rotates this Robot instance's direction to the RIGHT. For example, if currently + facing NORTH, the new direction will be EAST. + + Does nothing if this instance has not yet been correctly placed (initialized). + """ + if self.is_initialized(): + return + self._direction = (self._direction + 1) % 4 + + def __str__(self) -> str: + """ + Returns a string representation of the instance including position and + direction. + + Returns: + str: This instance's string representation. + """ + if not self.is_initialized(): + return "Uninitialized" + return "X: {}, Y: {}, direction: {}".format( + self._position_x, + self._position_y, + {v: k for k, v in Robot.DIRECTIONS.items()}[self._direction], + ) diff --git a/trmain.py b/trmain.py new file mode 100644 index 0000000..2369451 --- /dev/null +++ b/trmain.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# import sys +# import argparse + +import toyrobot + + +def main(): + current_bot = toyrobot.Robot(10, 10) + print(current_bot) + current_bot.place(6, 6, "NORTH") + print(current_bot) + current_bot.place(2, 2, "NORTH") + print(current_bot) + current_bot.place(6, 6, "NORTH") + print(current_bot) + print("Not yet implemented") + + +if __name__ == "__main__": + main()