Rain Risk
2020-12-12 Original Prompt Part 1
Another straightforward one! No tricks, no fancy algorithms, just following instructions. After day 11’s odd grid, I was personally happy to back to a more standard X/Y plane. In any case, let’s start with a class and some constants:
COMPASS = ["N", "E", "S", "W"]
class Ship: def __init__(self) -> None: self.x = 0 self.y = 0 # index in the compass self.facing = 0Our ship has two main actions, moving and rotating. Moving is more verbose, but simple.
Our grid is much like high school algebra: (0,0) is in the middle of an infinite plane. X increases in value to the right; Y increases when going up. The only interesting bit is that F is equal to a move in whatever direction we’re already facing:
def move(self, direction: str, distance: int): if direction == "F": direction = self.facing
if direction == "N": self.y += distance elif direction == "S": self.y -= distance elif direction == "E": self.x += distance elif direction == "W": self.x -= distanceRotating is slightly more interesting. The goal is to have a clean way to move through the COMPASS declared above. As a rule of thumb, whenever you have to loop through a finite list, the general pattern is new_index = (index + num_steps) % len(my_list). The % operator ensures our new_index never exceeds the length of the list, which is convenient. The rotate method translates the L and R commands into a number of steps (maybe negative) and calculates the new direction:
def rotate(self, direction: str, degrees: int): num_steps = degrees // 90 if direction == "L": num_steps *= -1
self.facing = (self.facing + num_steps) % len(COMPASS)Now all we need is to parse the input and string those functions together:
DIRECTIONS = set([*COMPASS, "F"])
def execute(self, instructions: List[str]): for instruction in instructions: move_type = instruction[0] value = int(instruction[1:]) if move_type in DIRECTIONS: self.move(move_type, value) else: self.rotate(move_type, value)Calculating the distance from 0 is easy with Python’s abs (absolute value) function:
def distance(self) -> int: return abs(self.x) + abs(self.y)Finally, our actual solution:
ship = Ship()ship.execute(self.input)return ship.distance()Part 2
Much like in previous days, day 2 requires us to modify our part 1 code and add some logic branches.
First, we add a few new variables to our class to hold which mode we’re in (waypoint or not):
def __init__(self, waypoint=False) -> None: ... self.waypoint = waypoint self.waypoint_x = 10 self.waypoint_y = 1Our move method has to now mostly move the waypoint coordinates instead of the ship. The notable change is that an F move in waypoint mode moves the distance times the offset. Otherwise, everything is familiar:
def move(self, direction: str, distance: int):if self.waypoint: if direction == "F": self.x += distance * self.waypoint_x self.y += distance * self.waypoint_y elif direction == "N": self.waypoint_y += distance elif direction == "S": self.waypoint_y -= distance elif direction == "E": self.waypoint_x += distance elif direction == "W": self.waypoint_x -= distanceelse: # part 1 code ...The part that too me the longest to wrap my head around was rotating the waypoint. I had to read the prompt about 8 times:
The waypoint [is at] 10 units east and 4 units north of the ship. R90 … [moves] it to 4 units east and 10 units south of the ship.
In numbers, our waypoint goes from (10, 4) to (4, -10). If we had rotated L instead, we would have gone from (10, 4) to (-4, 10). So each time we rotate, there are two steps:
- Swap values
- Swap positivity for either
XorY. Based on the examples above,Lrotation means we swap the (now)xvalue.Ris the opposite.
We have to do both of the above for each of the rotation steps. These rules lead to straightforward code:
def rotate(self, direction: str, degrees: int): num_steps = degrees // 90 if self.waypoint: for _ in range(num_steps): temp = self.waypoint_y self.waypoint_y = self.waypoint_x self.waypoint_x = temp # when turning L, the new X swaps sign # opposite is true for R if direction == "L": self.waypoint_x *= -1 else: self.waypoint_y *= -1
else: # part 1 code ...The solution code is nearly identical:
ship = Ship(waypoint=True)ship.execute(self.input)return ship.distance()