# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # [Advent of Code 2020: Day 12](https://adventofcode.com/2020/day/12)

# ## --- Day 12: Rain Risk ---
#
# Your ferry made decent progress toward the island, but the storm came in faster than anyone expected. The ferry needs to take **evasive actions**!
#
# Unfortunately, the ship's navigation computer seems to be malfunctioning; rather than giving a route directly to safety, it produced extremely circuitous instructions. When the captain uses the [PA system](https://en.wikipedia.org/wiki/Public_address_system) to ask if anyone can help, you quickly volunteer.
#
# The navigation instructions (your puzzle input) consists of a sequence of single-character **actions** paired with integer input **values**. After staring at them for a few minutes, you work out what they probably mean:
#
# *   Action **`N`** means to move **north** by the given value.
# *   Action **`S`** means to move **south** by the given value.
# *   Action **`E`** means to move **east** by the given value.
# *   Action **`W`** means to move **west** by the given value.
# *   Action **`L`** means to turn **left** the given number of degrees.
# *   Action **`R`** means to turn **right** the given number of degrees.
# *   Action **`F`** means to move **forward** by the given value in the direction the ship is currently facing.
#
# The ship starts by facing **east**. Only the `L` and `R` actions change the direction the ship is facing. (That is, if the ship is facing east and the next instruction is `N10`, the ship would move north 10 units, but would still move east if the following action were `F`.)
#
# For example:
#
# ```
# F10
# N3
# F7
# R90
# F11
#
# ```
#
# These instructions would be handled as follows:
#
# *   `F10` would move the ship 10 units east (because the ship starts by facing east) to **east 10, north 0**.
# *   `N3` would move the ship 3 units north to **east 10, north 3**.
# *   `F7` would move the ship another 7 units east (because the ship is still facing east) to **east 17, north 3**.
# *   `R90` would cause the ship to turn right by 90 degrees and face **south**; it remains at **east 17, north 3**.
# *   `F11` would move the ship 11 units south to **east 17, south 8**.
#
# At the end of these instructions, the ship's [Manhattan distance](https://en.wikipedia.org/wiki/Manhattan_distance) (sum of the absolute values of its east/west position and its north/south position) from its starting position is `17 + 8` = **`25`**.
#
# Figure out where the navigation instructions lead. **What is the Manhattan distance between that location and the ship's starting position?**

# +
import unittest
import re
from IPython.display import Markdown, display

from aoc_puzzle import AocPuzzle

class FerryNav(AocPuzzle):
    
    def parse_data(self, raw_data):
        data_lines = raw_data.split('\n')
        self.data = []
        for line in data_lines:
            m = re.match('(\w)(\d+)', line)
            self.data.append((m.group(1), int(m.group(2))))
        
        self.NORTH = 'N'
        self.SOUTH = 'S'
        self.EAST = 'E'
        self.WEST = 'W'
        self.TURN_LEFT = 'L'
        self.TURN_RIGHT = 'R'
        self.GO_FORWARD = 'F'
        
        self.HEADING_LIST = ['N','E','S','W']
        self.DEGREES_PER_HEADING = 90
        
        self.start_pos = (0,0)
        self.pos = self.start_pos
        self.heading = self.EAST
        
    def change_heading(self, val):
        start_index = self.HEADING_LIST.index(self.heading)
        
        hchange = val // self.DEGREES_PER_HEADING
        
        hindex = (start_index + hchange) % len(self.HEADING_LIST)
        
        self.heading = self.HEADING_LIST[hindex]      
        
    def do_action(self, action):
        move, val = action
        
        if move == self.GO_FORWARD:
            move = self.heading
        
        lat, lon = self.pos
        if move == self.NORTH:
            lon += val
        elif move == self.SOUTH:
            lon -= val
        elif move == self.EAST:
            lat += val
        elif move == self.WEST:
            lat -= val
        elif move == self.TURN_LEFT:
            self.change_heading(-val)
        elif move == self.TURN_RIGHT:
            self.change_heading(val)
        else:
            raise f'Unknown Action: {action}'
            
        self.pos = (lat,lon)
            
    def run(self, output=False, debug=False):
        
        self.debug = debug
        
        for action in self.data:
            if debug: print(f'Action: {action}')
            self.do_action(action)
            if debug: print(f'Pos: {self.pos}\n')
                
        lat, lon = self.pos
        
        result = abs(lat) + abs(lon)
        
        if output:
            display(Markdown(f'### Manhattan distance traveled: `{result}`'))            
        return result
        

class TestBasic(unittest.TestCase):

    def test_parse_data(self):
        in_data = 'F10\nN3\nF7\nR90\nF11'
        exp_out = [('F',10),('N',3),('F',7),('R',90),('F',11)]
        fn = FerryNav(in_data)
        self.assertEqual(fn.data, exp_out)
        
    def test_ferry_nav(self):
        in_data = 'F10\nN3\nF7\nR90\nF11'
        exp_out = 25
        fn = FerryNav(in_data)
        self.assertEqual(fn.run(debug=True), exp_out)
        
unittest.main(argv=[""], exit=False)
# -

fn = FerryNav("input/d12.txt")
fn.run(output=True)


# ## --- Part Two ---
#
# Before you can give the destination to the captain, you realize that the actual action meanings were printed on the back of the instructions the whole time.
#
# Almost all of the actions indicate how to move a **waypoint** which is relative to the ship's position:
#
# *   Action **`N`** means to move the waypoint **north** by the given value.
# *   Action **`S`** means to move the waypoint **south** by the given value.
# *   Action **`E`** means to move the waypoint **east** by the given value.
# *   Action **`W`** means to move the waypoint **west** by the given value.
# *   Action **`L`** means to rotate the waypoint around the ship **left** (**counter-clockwise**) the given number of degrees.
# *   Action **`R`** means to rotate the waypoint around the ship **right** (**clockwise**) the given number of degrees.
# *   Action **`F`** means to move **forward** to the waypoint a number of times equal to the given value.
#
# The waypoint starts **10 units east and 1 unit north** relative to the ship. The waypoint is relative to the ship; that is, if the ship moves, the waypoint moves with it.
#
# For example, using the same instructions as above:
#
# *   `F10` moves the ship to the waypoint 10 times (a total of **100 units east and 10 units north**), leaving the ship at **east 100, north 10**. The waypoint stays 10 units east and 1 unit north of the ship.
# *   `N3` moves the waypoint 3 units north to **10 units east and 4 units north of the ship**. The ship remains at **east 100, north 10**.
# *   `F7` moves the ship to the waypoint 7 times (a total of **70 units east and 28 units north**), leaving the ship at **east 170, north 38**. The waypoint stays 10 units east and 4 units north of the ship.
# *   `R90` rotates the waypoint around the ship clockwise 90 degrees, moving it to **4 units east and 10 units south of the ship**. The ship remains at **east 170, north 38**.
# *   `F11` moves the ship to the waypoint 11 times (a total of **44 units east and 110 units south**), leaving the ship at **east 214, south 72**. The waypoint stays 4 units east and 10 units south of the ship.
#
# After these operations, the ship's Manhattan distance from its starting position is `214 + 72` = **`286`**.
#
# Figure out where the navigation instructions actually lead. **What is the Manhattan distance between that location and the ship's starting position?**

# +
class FerryNav2(FerryNav):
    waypoint = (10,1)
    
    def do_move(self, mag):
        lat, lon = self.pos
        wp_lat, wp_lon = self.waypoint
        
        lat += wp_lat * mag
        lon += wp_lon * mag
        self.pos = (lat, lon)
        
    def rotate_waypoint(self, val):
        lat, lon = self.waypoint
        
        hchange = abs(val) // self.DEGREES_PER_HEADING
        if val > 0:
            for _ in range(hchange):
                lat, lon = lon, -lat
        else:
            for _ in range(hchange):
                lat, lon = -lon, lat
                
        return (lat, lon)
    
    def do_action(self, action):
        move, val = action
        
        if move == self.GO_FORWARD:
            self.do_move(val)
            return
        
        lat, lon = self.waypoint
        if move == self.NORTH:
            lon += val
        elif move == self.SOUTH:
            lon -= val
        elif move == self.EAST:
            lat += val
        elif move == self.WEST:
            lat -= val
        elif move == self.TURN_LEFT:
            lat, lon = self.rotate_waypoint(-val)
        elif move == self.TURN_RIGHT:
            lat, lon = self.rotate_waypoint(val)
        else:
            raise f'Unknown Action: {action}'
            
        self.waypoint = (lat,lon)
        if self.debug: print(f'Waypoint: {self.waypoint}')


class TestBasic(unittest.TestCase):
        
    def test_ferry_nav2(self):
        in_data = 'F10\nN3\nF7\nR90\nF11'
        exp_out = 286
        fn = FerryNav2(in_data)
        self.assertEqual(fn.run(debug=True), exp_out)
        
unittest.main(argv=[""], exit=False)
# -

fn = FerryNav2("input/d12.txt")
fn.run(output=True)

StarcoderdataJupyterScriptsDedupFiltered