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 = 0
Our 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 -= distance
Rotating 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 = 1
Our 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
X
orY
. Based on the examples above,L
rotation means we swap the (now)x
value.R
is 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()