style: changed all line endings to UNIX style

This commit is contained in:
Chris Davoren 2023-11-15 12:41:07 +10:00
parent 38f23b340c
commit ce34dc24cd
6 changed files with 715 additions and 715 deletions

View File

@ -1,34 +1,34 @@
# Feedback
Given 21/10/2023.
## Original Feedback
What was great:
1. A lot of comments, show excellent rigor. There are many people with many differing opinions on the value of comments, but I think in this instance it demonstrates a good professional practice.
1. It works.
1. Includes tests AND examples as well as a way to parse examples into the runtime of the robot. Nice!
What could have been better:
1. robot.set_limits doesnt appear to be necessary, also doesnt check that the mutation of the robot state does not invalidate the robot placement.
2. robot.get_limits doesnt appear to be necessary for the purposes of the exercise, and is currently unused.
3. Movement vectors is a good design decision imo. I think I did the same thing, it converts to semantics of direction to board placement.
4. Good use of specifications to return from an invalid request on movement
5. Rotation functions seems sensible and use modulus function to cycle within the domain of directions.
6. The interpret_command function seems sensible, although the specifications do not require cases insensitivity. Another oddity is that it appears it is only when an invalid PLACE command it attempted, the user is presented with text for valid commands, rather than a default case for the command switch statement. The requirement dont include informing users of valid commands.
7. The string representation of the robot state, use for describing its position should probably not return “uninitialised” if there is no board state, I believe that this is addressed in the requirements.
8. The test structure could use a little more rigor. BDD and AAA practices are pretty universal. One of the main problems of a test is communicating what functionality you are proving, and often we want to evaluate that against relevant requirement specifications. Naming which communicates which requirement it is proving is very helpful.
9. The examples and the tests execute as expected but there is not implementation of the main program loop. Part of the requirements is to take user input.
## Initial Response/Notes
1. Fair, and also a logical error/omission on my part. These functions were added with the mindset of more complete get/setter coverage, but I agree they're not needed for this problem. Removed.
2. As for (1)
3. N/A (but thanks)
4. N/A
5. N/A
6. Case sensitivity *was* implied by the examples, but it was not explicitly clarified one way or the other. Case insensivity was easy to implement and seemed to represent the most "open" interpretation of the requirements, or so I thought.
7. Agree that if one considers a "REPORT" command to be "invalid" if the board is uninitialized, then it should simply be discarded as per the requirements. I'm willing to be persuaded otherwise, but I believe my line of thinking at the time was that REPORT is not an invalid *command*, but called on an invalid *state*, so it should behave sensibly in that setting.
8. Totally fair, the test structure is definitely quite *ad hoc*. Honestly formalised testing is one of my weak areas, I suppose a consequence of most of my experience coming from a research context. I will look more closely at BDD and AAA, thank you.
9. It would be easy enough to implement this, although there is no interactive input loop mentioned in the requirements I was working off. The README shows the version of the requirements the solution is based off.
# Feedback
Given 21/10/2023.
## Original Feedback
What was great:
1. A lot of comments, show excellent rigor. There are many people with many differing opinions on the value of comments, but I think in this instance it demonstrates a good professional practice.
1. It works.
1. Includes tests AND examples as well as a way to parse examples into the runtime of the robot. Nice!
What could have been better:
1. robot.set_limits doesnt appear to be necessary, also doesnt check that the mutation of the robot state does not invalidate the robot placement.
2. robot.get_limits doesnt appear to be necessary for the purposes of the exercise, and is currently unused.
3. Movement vectors is a good design decision imo. I think I did the same thing, it converts to semantics of direction to board placement.
4. Good use of specifications to return from an invalid request on movement
5. Rotation functions seems sensible and use modulus function to cycle within the domain of directions.
6. The interpret_command function seems sensible, although the specifications do not require cases insensitivity. Another oddity is that it appears it is only when an invalid PLACE command it attempted, the user is presented with text for valid commands, rather than a default case for the command switch statement. The requirement dont include informing users of valid commands.
7. The string representation of the robot state, use for describing its position should probably not return “uninitialised” if there is no board state, I believe that this is addressed in the requirements.
8. The test structure could use a little more rigor. BDD and AAA practices are pretty universal. One of the main problems of a test is communicating what functionality you are proving, and often we want to evaluate that against relevant requirement specifications. Naming which communicates which requirement it is proving is very helpful.
9. The examples and the tests execute as expected but there is not implementation of the main program loop. Part of the requirements is to take user input.
## Initial Response/Notes
1. Fair, and also a logical error/omission on my part. These functions were added with the mindset of more complete get/setter coverage, but I agree they're not needed for this problem. Removed.
2. As for (1)
3. N/A (but thanks)
4. N/A
5. N/A
6. Case sensitivity *was* implied by the examples, but it was not explicitly clarified one way or the other. Case insensivity was easy to implement and seemed to represent the most "open" interpretation of the requirements, or so I thought.
7. Agree that if one considers a "REPORT" command to be "invalid" if the board is uninitialized, then it should simply be discarded as per the requirements. I'm willing to be persuaded otherwise, but I believe my line of thinking at the time was that REPORT is not an invalid *command*, but called on an invalid *state*, so it should behave sensibly in that setting.
8. Totally fair, the test structure is definitely quite *ad hoc*. Honestly formalised testing is one of my weak areas, I suppose a consequence of most of my experience coming from a research context. I will look more closely at BDD and AAA, thank you.
9. It would be easy enough to implement this, although there is no interactive input loop mentioned in the requirements I was working off. The README shows the version of the requirements the solution is based off.

222
README.md
View File

@ -1,111 +1,111 @@
# Toy Robot Simulator
## Problem Statement
Initial problem description taken from https://github.com/xandeep/ToyRobot.
Create a library that can read in commands of the following form:
```
PLACE X,Y,DIRECTION
MOVE
LEFT
RIGHT
REPORT
```
*The library allows for a simulation of a toy robot moving on a 6 x 6 square tabletop.*
1. There are no obstructions on the table surface.
2. The robot is free to roam around the surface of the table, but must be prevented from falling to destruction. Any movement that would result in this must be prevented, however further valid movement commands must still be allowed.
3. PLACE will put the toy robot on the table in position X,Y and facing NORTH, SOUTH, EAST or WEST.
4. (0,0) can be considered as the SOUTH WEST corner and (5,5) as the NORTH EAST corner.
5. The first valid command to the robot is a PLACE command. After that, any sequence of commands may be issued, in any order, including another PLACE command. The library should discard all commands in the sequence until a valid PLACE command has been executed.
6. The PLACE command should be discarded if it places the robot outside of the table surface.
7. Once the robot is on the table, subsequent PLACE commands could leave out the direction and only provide the coordinates. When this happens, the robot moves to the new coordinates without changing the direction.
8. MOVE will move the toy robot one unit forward in the direction it is currently facing.
9. LEFT and RIGHT will rotate the robot 90 degrees in the specified direction without changing the position of the robot.
10. REPORT will announce the X,Y and orientation of the robot.
11. A robot that is not on the table can choose to ignore the MOVE, LEFT, RIGHT and REPORT commands.
12. The library should discard all invalid commands and parameters.
### Example Input and Output:
```
a)
PLACE 0,0,NORTH
MOVE
REPORT
Output: 0,1,NORTH
```
```
b)
PLACE 0,0,NORTH
LEFT
REPORT
Output: 0,0,WEST
```
```
c)
PLACE 1,2,EAST
MOVE
MOVE
LEFT
MOVE
REPORT
Output: 3,3,NORTH
```
```
d)
PLACE 1,2,EAST
MOVE
LEFT
MOVE
PLACE 3,1
MOVE
REPORT
Output: 3,2,NORTH
```
### Design
This is a relatively simple programming problem that one might expect to find as part of an introductory tertiary programming course. It focusses mainly on data representation, state tracking and modification, constraint checking, and some text processing in a text-based environment. Error handling is de-emphasised as most invalid inputs are discarded and one may assume that initial invalid states are not considered. File handling is not required, though it has been used in this implementation for static test data.
Initial thoughts on approach included the use of a basic state machine, but movement and coordinate tracking don't lend themselves neatly to a state machine architecture, and so a more tradaitional encapsulated state was used instead.
The following additional considerations were made:
- Command-line Python was chosen as the language of choice simply due to my familiarity with it. This problem could likely be implemented in any language or environment that supports an equivalent concept of "library", including functional languages.
- The library (module in Python terminology) `toyrobot` contains just one class, `Robot`, that is aware of both its movement limits (configurable, but defaults to the 6x6 space in the original description) and can also interpret text commands. This monolithic design is appropriate given the simplicity of the problem as provided, but further separation of concerns may be desirable in a more complicated context. For instance, if the problem were to require tracking of more than one robot, it may make sense to abstract out the movement domain and command interpreter as independent entities.
- As per the spirit of the description, almost all methods are designed to "fail silently" (i.e. leave the Robot state unchanged) if they are invalid or are given invalid inputs. The one exception to this is the ``__init__`` constructor - if numerically invalid maximums are provided for movement limits then a `ValueError` exception is thrown. This ensures that a Robot instance cannot exist with an invalid initial state or configuration.
## Installation and Pre-requisites
Python version 3.10 or above is required due to the use of the `match/case` construct.
No external libraries are used, and a Python virtual environment is not necessary.
## Usage
To run the examples listed in the problem description:
```
python trexamples.py
```
## Unit Tests
A number of basic unit tests are included using the internal Python library `unittest`. These are not exhaustive, but do provide testing of the expected behaviour of the essential methods of the `Robot` class. For ease of reference these are all included in a single class, although it is worth noting that as per the `unittest` [documentation](https://docs.python.org/3/library/unittest.html) many of these should technically be in separate classes with individually specified "setup" and "takedown" code.
To run the tests, use the command:
```
python -m unittest -v trtests.py
```
### Author
Chris Davoren, 2023, cdavoren@gmail.com
# Toy Robot Simulator
## Problem Statement
Initial problem description taken from https://github.com/xandeep/ToyRobot.
Create a library that can read in commands of the following form:
```
PLACE X,Y,DIRECTION
MOVE
LEFT
RIGHT
REPORT
```
*The library allows for a simulation of a toy robot moving on a 6 x 6 square tabletop.*
1. There are no obstructions on the table surface.
2. The robot is free to roam around the surface of the table, but must be prevented from falling to destruction. Any movement that would result in this must be prevented, however further valid movement commands must still be allowed.
3. PLACE will put the toy robot on the table in position X,Y and facing NORTH, SOUTH, EAST or WEST.
4. (0,0) can be considered as the SOUTH WEST corner and (5,5) as the NORTH EAST corner.
5. The first valid command to the robot is a PLACE command. After that, any sequence of commands may be issued, in any order, including another PLACE command. The library should discard all commands in the sequence until a valid PLACE command has been executed.
6. The PLACE command should be discarded if it places the robot outside of the table surface.
7. Once the robot is on the table, subsequent PLACE commands could leave out the direction and only provide the coordinates. When this happens, the robot moves to the new coordinates without changing the direction.
8. MOVE will move the toy robot one unit forward in the direction it is currently facing.
9. LEFT and RIGHT will rotate the robot 90 degrees in the specified direction without changing the position of the robot.
10. REPORT will announce the X,Y and orientation of the robot.
11. A robot that is not on the table can choose to ignore the MOVE, LEFT, RIGHT and REPORT commands.
12. The library should discard all invalid commands and parameters.
### Example Input and Output:
```
a)
PLACE 0,0,NORTH
MOVE
REPORT
Output: 0,1,NORTH
```
```
b)
PLACE 0,0,NORTH
LEFT
REPORT
Output: 0,0,WEST
```
```
c)
PLACE 1,2,EAST
MOVE
MOVE
LEFT
MOVE
REPORT
Output: 3,3,NORTH
```
```
d)
PLACE 1,2,EAST
MOVE
LEFT
MOVE
PLACE 3,1
MOVE
REPORT
Output: 3,2,NORTH
```
### Design
This is a relatively simple programming problem that one might expect to find as part of an introductory tertiary programming course. It focusses mainly on data representation, state tracking and modification, constraint checking, and some text processing in a text-based environment. Error handling is de-emphasised as most invalid inputs are discarded and one may assume that initial invalid states are not considered. File handling is not required, though it has been used in this implementation for static test data.
Initial thoughts on approach included the use of a basic state machine, but movement and coordinate tracking don't lend themselves neatly to a state machine architecture, and so a more tradaitional encapsulated state was used instead.
The following additional considerations were made:
- Command-line Python was chosen as the language of choice simply due to my familiarity with it. This problem could likely be implemented in any language or environment that supports an equivalent concept of "library", including functional languages.
- The library (module in Python terminology) `toyrobot` contains just one class, `Robot`, that is aware of both its movement limits (configurable, but defaults to the 6x6 space in the original description) and can also interpret text commands. This monolithic design is appropriate given the simplicity of the problem as provided, but further separation of concerns may be desirable in a more complicated context. For instance, if the problem were to require tracking of more than one robot, it may make sense to abstract out the movement domain and command interpreter as independent entities.
- As per the spirit of the description, almost all methods are designed to "fail silently" (i.e. leave the Robot state unchanged) if they are invalid or are given invalid inputs. The one exception to this is the ``__init__`` constructor - if numerically invalid maximums are provided for movement limits then a `ValueError` exception is thrown. This ensures that a Robot instance cannot exist with an invalid initial state or configuration.
## Installation and Pre-requisites
Python version 3.10 or above is required due to the use of the `match/case` construct.
No external libraries are used, and a Python virtual environment is not necessary.
## Usage
To run the examples listed in the problem description:
```
python trexamples.py
```
## Unit Tests
A number of basic unit tests are included using the internal Python library `unittest`. These are not exhaustive, but do provide testing of the expected behaviour of the essential methods of the `Robot` class. For ease of reference these are all included in a single class, although it is worth noting that as per the `unittest` [documentation](https://docs.python.org/3/library/unittest.html) many of these should technically be in separate classes with individually specified "setup" and "takedown" code.
To run the tests, use the command:
```
python -m unittest -v trtests.py
```
### Author
Chris Davoren, 2023, cdavoren@gmail.com

View File

@ -1,3 +1,3 @@
# __init__.py
from .robot import Robot
# __init__.py
from .robot import Robot

View File

@ -1,349 +1,349 @@
class Robot:
"""
Class representing a single "Toy Robot". Robots instances have knowledge
of their position, direction, and movement limits (i.e. "board" size).
Note: Direction is specified via compass points (north, west, etc). As per
the original problem description, the coordinates (0, 0) represent the
SOUTHWEST corner of the board.
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.
VALID_COMMANDS (list): A list of valid commands that can be interpreted
by the robot. The PLACE command requires at least two additional
parameters. See the complete problem description for more details.
"""
DEFAULT_MAX_X = 5
DEFAULT_MAX_Y = 5
# Directions ordered such that adding 1 corresponds to RIGHT turn
DIRECTIONS = {
"NORTH": 0,
"NORTHEAST": 1,
"EAST": 2,
"SOUTHEAST": 3,
"SOUTH": 4,
"SOUTHWEST": 5,
"WEST": 6,
"NORTHWEST": 7,
}
VALID_COMMANDS = ["BLOCK", "PLACE", "MOVE", "LEFT", "RIGHT", "REPORT"]
# 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": 1},
2: {"x": 1, "y": 0},
3: {"x": 1, "y": -1},
4: {"x": 0, "y": -1},
5: {"x": -1, "y": -1},
6: {"x": -1, "y": 0},
7: {"x": -1, "y": 1},
}
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
self._blocks = []
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.
"""
within_limits = x >= 0 and y >= 0 and x <= self._max_x and y <= self._max_y
on_block = (x, y) in self._blocks
# print(self._blocks)
# print(within_limits)
# print(on_block)
return within_limits and not on_block
def add_block(self, x: int, y: int):
if x == self._position_x and y == self._position_y:
return
self._blocks.append((x, 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 (fail silently) 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 self.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 = None):
"""
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`. Must be specified on first
placement (or this call will fail silently), but is optional
thereafter.
"""
# Must be careful not to make any state changes until all inputs
# have been validated
# print('Checking position...')
if not self.valid_position(position_x, position_y):
return
# print('Position check passed.')
if direction_name is not None:
direction_name = direction_name.upper()
if direction_name not in Robot.DIRECTIONS.keys():
return
elif not self.is_initialized():
# Direction MUST be specified on first placement
return
if direction_name is not None:
self._direction = Robot.DIRECTIONS[direction_name]
self._position_x = position_x
self._position_y = position_y
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) % len(Robot.DIRECTIONS)
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 not self.is_initialized():
return
self._direction = (self._direction + 1) % len(Robot.DIRECTIONS)
def interpret_command(self, command: str):
"""
Interprets a given string command and applies the appropriate
transformation to this Robot instance. Fails silently if the command
is unrecognized or invalid.
Refer to the full problem description for a list and examples of valid
commands.
Args:
command (str): The command to be interpreted.
"""
command = command.upper()
command_tokens = [x.strip() for x in command.split(" ") if len(x) > 0]
# This is not strictly necessary as case
if len(command_tokens) == 0 or not command_tokens[0] in Robot.VALID_COMMANDS:
return
match command_tokens[0]:
case "BLOCK":
# print('Found block...')
try:
if len(command_tokens) < 2:
# print('Insufficient tokens...')
return
parameter_tokens = [x.strip() for x in command_tokens[1].split(",")]
if len(parameter_tokens) < 2:
# print('Insufficient parameters...')
return
block_x = int(parameter_tokens[0])
block_y = int(parameter_tokens[1])
# print("Adding block: {} {}".format(block_x, block_y))
self.add_block(block_x, block_y)
except ValueError:
# print('Integer parsing error...')
return
case "PLACE":
try:
# Must have parameters
if len(command_tokens) < 2:
return
parameter_tokens = [x.strip() for x in command_tokens[1].split(",")]
# Must have at least X, Y
if len(parameter_tokens) < 2:
return
# Throws ValueError if invalid input
place_x = int(parameter_tokens[0])
place_y = int(parameter_tokens[1])
# Direction parameter is optional on second and subsequent
# placements. The place() method accounts for an absent
# direction on first call and fails silently.
# print("PLACE command parsed: {}, {}".format(place_x, place_y))
if len(parameter_tokens) > 2:
place_direction = parameter_tokens[2]
self.place(place_x, place_y, place_direction)
else:
self.place(place_x, place_y)
except ValueError:
# Unable to convert x or y token to int
return
case "MOVE":
self.move()
case "LEFT":
self.rotate_left()
case "RIGHT":
self.rotate_right()
case "REPORT":
print("Output: {}".format(str(self)))
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 "{},{},{} | BLOCKS : {}".format(
self._position_x,
self._position_y,
self.get_direction(),
":".join([str(x) for x in self._blocks]),
)
class Robot:
"""
Class representing a single "Toy Robot". Robots instances have knowledge
of their position, direction, and movement limits (i.e. "board" size).
Note: Direction is specified via compass points (north, west, etc). As per
the original problem description, the coordinates (0, 0) represent the
SOUTHWEST corner of the board.
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.
VALID_COMMANDS (list): A list of valid commands that can be interpreted
by the robot. The PLACE command requires at least two additional
parameters. See the complete problem description for more details.
"""
DEFAULT_MAX_X = 5
DEFAULT_MAX_Y = 5
# Directions ordered such that adding 1 corresponds to RIGHT turn
DIRECTIONS = {
"NORTH": 0,
"NORTHEAST": 1,
"EAST": 2,
"SOUTHEAST": 3,
"SOUTH": 4,
"SOUTHWEST": 5,
"WEST": 6,
"NORTHWEST": 7,
}
VALID_COMMANDS = ["BLOCK", "PLACE", "MOVE", "LEFT", "RIGHT", "REPORT"]
# 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": 1},
2: {"x": 1, "y": 0},
3: {"x": 1, "y": -1},
4: {"x": 0, "y": -1},
5: {"x": -1, "y": -1},
6: {"x": -1, "y": 0},
7: {"x": -1, "y": 1},
}
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
self._blocks = []
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.
"""
within_limits = x >= 0 and y >= 0 and x <= self._max_x and y <= self._max_y
on_block = (x, y) in self._blocks
# print(self._blocks)
# print(within_limits)
# print(on_block)
return within_limits and not on_block
def add_block(self, x: int, y: int):
if x == self._position_x and y == self._position_y:
return
self._blocks.append((x, 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 (fail silently) 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 self.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 = None):
"""
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`. Must be specified on first
placement (or this call will fail silently), but is optional
thereafter.
"""
# Must be careful not to make any state changes until all inputs
# have been validated
# print('Checking position...')
if not self.valid_position(position_x, position_y):
return
# print('Position check passed.')
if direction_name is not None:
direction_name = direction_name.upper()
if direction_name not in Robot.DIRECTIONS.keys():
return
elif not self.is_initialized():
# Direction MUST be specified on first placement
return
if direction_name is not None:
self._direction = Robot.DIRECTIONS[direction_name]
self._position_x = position_x
self._position_y = position_y
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) % len(Robot.DIRECTIONS)
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 not self.is_initialized():
return
self._direction = (self._direction + 1) % len(Robot.DIRECTIONS)
def interpret_command(self, command: str):
"""
Interprets a given string command and applies the appropriate
transformation to this Robot instance. Fails silently if the command
is unrecognized or invalid.
Refer to the full problem description for a list and examples of valid
commands.
Args:
command (str): The command to be interpreted.
"""
command = command.upper()
command_tokens = [x.strip() for x in command.split(" ") if len(x) > 0]
# This is not strictly necessary as case
if len(command_tokens) == 0 or not command_tokens[0] in Robot.VALID_COMMANDS:
return
match command_tokens[0]:
case "BLOCK":
# print('Found block...')
try:
if len(command_tokens) < 2:
# print('Insufficient tokens...')
return
parameter_tokens = [x.strip() for x in command_tokens[1].split(",")]
if len(parameter_tokens) < 2:
# print('Insufficient parameters...')
return
block_x = int(parameter_tokens[0])
block_y = int(parameter_tokens[1])
# print("Adding block: {} {}".format(block_x, block_y))
self.add_block(block_x, block_y)
except ValueError:
# print('Integer parsing error...')
return
case "PLACE":
try:
# Must have parameters
if len(command_tokens) < 2:
return
parameter_tokens = [x.strip() for x in command_tokens[1].split(",")]
# Must have at least X, Y
if len(parameter_tokens) < 2:
return
# Throws ValueError if invalid input
place_x = int(parameter_tokens[0])
place_y = int(parameter_tokens[1])
# Direction parameter is optional on second and subsequent
# placements. The place() method accounts for an absent
# direction on first call and fails silently.
# print("PLACE command parsed: {}, {}".format(place_x, place_y))
if len(parameter_tokens) > 2:
place_direction = parameter_tokens[2]
self.place(place_x, place_y, place_direction)
else:
self.place(place_x, place_y)
except ValueError:
# Unable to convert x or y token to int
return
case "MOVE":
self.move()
case "LEFT":
self.rotate_left()
case "RIGHT":
self.rotate_right()
case "REPORT":
print("Output: {}".format(str(self)))
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 "{},{},{} | BLOCKS : {}".format(
self._position_x,
self._position_y,
self.get_direction(),
":".join([str(x) for x in self._blocks]),
)

View File

@ -1,41 +1,41 @@
#!/usr/bin/env python3
import toyrobot
def feed_file(filename: str, robot: toyrobot.Robot):
with open(filename) as f:
for line in f:
line = line.strip()
print(line)
robot.interpret_command(line)
def main():
print("a)")
feed_file("example_a.txt", toyrobot.Robot())
print()
print("b)")
feed_file("example_b.txt", toyrobot.Robot())
print()
print("c)")
feed_file("example_c.txt", toyrobot.Robot())
print()
print("d)")
feed_file("example_d.txt", toyrobot.Robot())
print()
print("e)")
feed_file("example_e.txt", toyrobot.Robot())
print()
print("f)")
feed_file("example_f.txt", toyrobot.Robot())
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import toyrobot
def feed_file(filename: str, robot: toyrobot.Robot):
with open(filename) as f:
for line in f:
line = line.strip()
print(line)
robot.interpret_command(line)
def main():
print("a)")
feed_file("example_a.txt", toyrobot.Robot())
print()
print("b)")
feed_file("example_b.txt", toyrobot.Robot())
print()
print("c)")
feed_file("example_c.txt", toyrobot.Robot())
print()
print("d)")
feed_file("example_d.txt", toyrobot.Robot())
print()
print("e)")
feed_file("example_e.txt", toyrobot.Robot())
print()
print("f)")
feed_file("example_f.txt", toyrobot.Robot())
print()
if __name__ == "__main__":
main()

View File

@ -1,177 +1,177 @@
#!/usr/bin/env python3
import unittest
import toyrobot
class ToyRobotTests(unittest.TestCase):
def test_creation(self):
robot = toyrobot.Robot()
self.assertFalse(robot.is_initialized())
def test_initialization(self):
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
self.assertTrue(robot.is_initialized())
def test_invalid_max_x(self):
exception_thrown = False
try:
toyrobot.Robot(-1, 5)
except ValueError:
exception_thrown = True
self.assertTrue(exception_thrown)
def test_invalid_max_y(self):
exception_thrown = False
try:
toyrobot.Robot(5, -1)
except ValueError:
exception_thrown = True
self.assertTrue(exception_thrown)
def test_valid_place(self):
robot = toyrobot.Robot()
robot.place(1, 1, "NORTH")
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (1, 1)
and robot.get_direction() == "NORTH"
)
def test_invalid_place_position_x(self):
robot = toyrobot.Robot()
robot.place(6, 1, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_position_y(self):
robot = toyrobot.Robot()
robot.place(1, 6, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_position_both(self):
robot = toyrobot.Robot()
robot.place(6, 6, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_direction(self):
robot = toyrobot.Robot()
robot.place(1, 1, "INVALID")
self.assertFalse(robot.is_initialized())
def test_missing_place_direction(self):
robot = toyrobot.Robot()
robot.place(1, 1)
self.assertFalse(robot.is_initialized())
def test_double_place(self):
robot = toyrobot.Robot()
robot.place(5, 5, "NORTH")
robot.place(2, 2)
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (2, 2)
and robot.get_direction() == "NORTH"
)
def test_double_place_2(self):
robot = toyrobot.Robot()
robot.place(5, 5, "NORTH")
robot.place(2, 2, "SOUTH")
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (2, 2)
and robot.get_direction() == "SOUTH"
)
def test_valid_movement_x(self):
robot = toyrobot.Robot()
robot.place(2, 2, "EAST")
robot.move()
self.assertTrue(robot.get_position() == (3, 2))
def test_valid_movement_y(self):
robot = toyrobot.Robot()
robot.place(2, 2, "NORTH")
robot.move()
self.assertTrue(robot.get_position() == (2, 3))
def test_invalid_movement_north(self):
robot = toyrobot.Robot()
robot.place(1, 5, "NORTH")
robot.move()
self.assertTrue(robot.get_position() == (1, 5))
def test_invalid_movement_east(self):
robot = toyrobot.Robot()
robot.place(5, 1, "EAST")
robot.move()
self.assertTrue(robot.get_position() == (5, 1))
def test_invalid_movement_south(self):
robot = toyrobot.Robot()
robot.place(1, 0, "SOUTH")
robot.move()
self.assertTrue(robot.get_position() == (1, 0))
def test_invalid_movement_west(self):
robot = toyrobot.Robot()
robot.place(0, 1, "WEST")
robot.move()
self.assertTrue(robot.get_position() == (0, 1))
def test_rotate_left(self):
robot = toyrobot.Robot()
robot.place(0, 0, "SOUTH")
robot.rotate_left()
self.assertTrue(robot.get_direction() == "EAST")
def test_rotate_left_2(self):
# Blackbox test - ensures internal numerical direction using modulus
# correctly on decrement.
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
robot.rotate_left()
self.assertTrue(robot.get_direction() == "WEST")
def test_rotate_right(self):
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
robot.rotate_right()
self.assertTrue(robot.get_direction() == "EAST")
def test_rotate_right_2(self):
# Blackbox test - ensures internal numerical direction using modulus
# correctly on increment.
robot = toyrobot.Robot()
robot.place(0, 0, "WEST")
robot.rotate_right()
self.assertTrue(robot.get_direction() == "NORTH")
def test_example_d_calls(self):
robot = toyrobot.Robot()
robot.place(1, 2, "EAST")
robot.move()
robot.rotate_left()
robot.move()
robot.place(3, 1)
robot.move()
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (3, 2)
and robot.get_direction() == "NORTH"
)
def test_example_d_commands(self):
robot = toyrobot.Robot()
robot.interpret_command("PLACE 1,2,EAST")
robot.interpret_command("MOVE")
robot.interpret_command("LEFT")
robot.interpret_command("MOVE")
robot.interpret_command("PLACE 3,1")
robot.interpret_command("MOVE")
self.assertTrue(str(robot) == "3,2,NORTH")
if __name__ == "__main__":
unittest.main()
#!/usr/bin/env python3
import unittest
import toyrobot
class ToyRobotTests(unittest.TestCase):
def test_creation(self):
robot = toyrobot.Robot()
self.assertFalse(robot.is_initialized())
def test_initialization(self):
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
self.assertTrue(robot.is_initialized())
def test_invalid_max_x(self):
exception_thrown = False
try:
toyrobot.Robot(-1, 5)
except ValueError:
exception_thrown = True
self.assertTrue(exception_thrown)
def test_invalid_max_y(self):
exception_thrown = False
try:
toyrobot.Robot(5, -1)
except ValueError:
exception_thrown = True
self.assertTrue(exception_thrown)
def test_valid_place(self):
robot = toyrobot.Robot()
robot.place(1, 1, "NORTH")
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (1, 1)
and robot.get_direction() == "NORTH"
)
def test_invalid_place_position_x(self):
robot = toyrobot.Robot()
robot.place(6, 1, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_position_y(self):
robot = toyrobot.Robot()
robot.place(1, 6, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_position_both(self):
robot = toyrobot.Robot()
robot.place(6, 6, "NORTH")
self.assertFalse(robot.is_initialized())
def test_invalid_place_direction(self):
robot = toyrobot.Robot()
robot.place(1, 1, "INVALID")
self.assertFalse(robot.is_initialized())
def test_missing_place_direction(self):
robot = toyrobot.Robot()
robot.place(1, 1)
self.assertFalse(robot.is_initialized())
def test_double_place(self):
robot = toyrobot.Robot()
robot.place(5, 5, "NORTH")
robot.place(2, 2)
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (2, 2)
and robot.get_direction() == "NORTH"
)
def test_double_place_2(self):
robot = toyrobot.Robot()
robot.place(5, 5, "NORTH")
robot.place(2, 2, "SOUTH")
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (2, 2)
and robot.get_direction() == "SOUTH"
)
def test_valid_movement_x(self):
robot = toyrobot.Robot()
robot.place(2, 2, "EAST")
robot.move()
self.assertTrue(robot.get_position() == (3, 2))
def test_valid_movement_y(self):
robot = toyrobot.Robot()
robot.place(2, 2, "NORTH")
robot.move()
self.assertTrue(robot.get_position() == (2, 3))
def test_invalid_movement_north(self):
robot = toyrobot.Robot()
robot.place(1, 5, "NORTH")
robot.move()
self.assertTrue(robot.get_position() == (1, 5))
def test_invalid_movement_east(self):
robot = toyrobot.Robot()
robot.place(5, 1, "EAST")
robot.move()
self.assertTrue(robot.get_position() == (5, 1))
def test_invalid_movement_south(self):
robot = toyrobot.Robot()
robot.place(1, 0, "SOUTH")
robot.move()
self.assertTrue(robot.get_position() == (1, 0))
def test_invalid_movement_west(self):
robot = toyrobot.Robot()
robot.place(0, 1, "WEST")
robot.move()
self.assertTrue(robot.get_position() == (0, 1))
def test_rotate_left(self):
robot = toyrobot.Robot()
robot.place(0, 0, "SOUTH")
robot.rotate_left()
self.assertTrue(robot.get_direction() == "EAST")
def test_rotate_left_2(self):
# Blackbox test - ensures internal numerical direction using modulus
# correctly on decrement.
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
robot.rotate_left()
self.assertTrue(robot.get_direction() == "WEST")
def test_rotate_right(self):
robot = toyrobot.Robot()
robot.place(0, 0, "NORTH")
robot.rotate_right()
self.assertTrue(robot.get_direction() == "EAST")
def test_rotate_right_2(self):
# Blackbox test - ensures internal numerical direction using modulus
# correctly on increment.
robot = toyrobot.Robot()
robot.place(0, 0, "WEST")
robot.rotate_right()
self.assertTrue(robot.get_direction() == "NORTH")
def test_example_d_calls(self):
robot = toyrobot.Robot()
robot.place(1, 2, "EAST")
robot.move()
robot.rotate_left()
robot.move()
robot.place(3, 1)
robot.move()
self.assertTrue(
robot.is_initialized()
and robot.get_position() == (3, 2)
and robot.get_direction() == "NORTH"
)
def test_example_d_commands(self):
robot = toyrobot.Robot()
robot.interpret_command("PLACE 1,2,EAST")
robot.interpret_command("MOVE")
robot.interpret_command("LEFT")
robot.interpret_command("MOVE")
robot.interpret_command("PLACE 3,1")
robot.interpret_command("MOVE")
self.assertTrue(str(robot) == "3,2,NORTH")
if __name__ == "__main__":
unittest.main()