zerosleeps

Since 2010

Advent of Code 2020 day 12

Advent of Code 2020 day 12. This one was really good fun - my favourite yet. Of course, as soon as I’d finished cleaning up I had a look at some of the solutions other folks had come up, and immediately felt a bit dumb. Seems I overcooked the rotation thing with all that trigonometry. Sod ‘em: I’m still very pleased with the structure and readability of my solution!

from math import radians, sin, cos
from pathlib import Path

class Point:
    FACINGS = ('north', 'east', 'south', 'west')

    # +-------> +x
    # |
    # |
    # |
    # +y

    def move(self, direction, distance):
        if direction == 'north':
            self.coordinates[1] -= distance
        elif direction == 'south':
            self.coordinates[1] += distance
        elif direction == 'east':
            self.coordinates[0] += distance
        elif direction == 'west':
            self.coordinates[0] -= distance

    def run_instruction(self, instruction):
        value = int(instruction[1:])

        if instruction[0] == 'N':
            self.move('north', value)
        elif instruction[0] == 'S':
            self.move('south', value)
        elif instruction[0] == 'E':
            self.move('east', value)
        elif instruction[0] == 'W':
            self.move('west', value)
        elif instruction[0] == 'L':
            self.rotate(-value)
        elif instruction[0] == 'R':
            self.rotate(value)
        elif instruction[0] == 'F':
            self.move(self.facing, value)

    def normalise_rotation(self, degrees):
        if degrees < 0:
            return degrees + 360
        else:
            return degrees

    def manhattan_distance(self):
        return sum([abs(c) for c in self.coordinates])

class Ship(Point):
    def __init__(self, version=1):
        self.coordinates = [0, 0]
        self.facing = 'east'

    def rotate(self, degrees):
        self.facing = self.FACINGS[(self.FACINGS.index(self.facing) +
                      int(self.normalise_rotation(degrees)/90))%4]

class Waypoint(Point):
    def __init__(self):
        self.coordinates = [10,-1]
        self.ship = Ship()

    def rotate(self, degrees):
        r = radians(self.normalise_rotation(degrees))

        self.coordinates = [
            round(cos(r) * (self.coordinates[0] - self.ship.coordinates[0]) -
                  sin(r) * (self.coordinates[1] - self.ship.coordinates[1]) +
                  self.ship.coordinates[0]),
            round(sin(r) * (self.coordinates[0] - self.ship.coordinates[0]) +
                  cos(r) * (self.coordinates[1] - self.ship.coordinates[1]) +
                  self.ship.coordinates[1])
        ]

    def distance_from_ship(self):
        return [
            self.coordinates[0] - self.ship.coordinates[0],
            self.coordinates[1] - self.ship.coordinates[1]
        ]

    def run_instruction(self, instruction):
        if instruction[0] == 'F':
            for _ in range(int(instruction[1:])):
                # Store waypoint's current distance from ship
                distance = self.distance_from_ship()
                # Move ship to waypoint
                self.ship.coordinates = self.coordinates
                # Move waypoint to new relative distance
                self.coordinates = [
                    self.ship.coordinates[0] + distance[0],
                    self.ship.coordinates[1] + distance[1]
                ]
        else:
            super().run_instruction(instruction)

def get_raw_input():
    return (Path(__file__).parent/'day_12_input.txt').read_text()

def parse_raw_input(raw_input):
    return [line.strip() for line in raw_input.strip().splitlines()]

def part_one(input):
    ship = Ship()
    for instruction in input:
        ship.run_instruction(instruction)
    return ship.manhattan_distance()

def part_two(input):
    waypoint = Waypoint()
    for instruction in input:
        waypoint.run_instruction(instruction)
    return waypoint.ship.manhattan_distance()

if __name__ == '__main__':
    print(f'Part one: {part_one(parse_raw_input(get_raw_input()))}')
    print(f'Part two: {part_two(parse_raw_input(get_raw_input()))}')