|
|
|
@@ -0,0 +1,190 @@ |
|
|
|
# encoding: utf-8 |
|
|
|
|
|
|
|
########################### |
|
|
|
# Advent of Code Day 1-2 # |
|
|
|
########################### |
|
|
|
# Combination lock - rotary dial |
|
|
|
# Instructions come in the form `DN+`, |
|
|
|
# where D is a direction L or R, and N+ is a series of integers of |
|
|
|
# arbitrary length. (e.g. L32, R947) |
|
|
|
# The dial is 0-indexed and has 100 positions (0-99). |
|
|
|
# Rotating left decrements the index; rotating right increments it. |
|
|
|
# Rotating left from 0 goes to 99, and rotating right from 99 goes to 0 |
|
|
|
# (i.e. it wraps around). |
|
|
|
# Count the number of times the dial ever sees 0. |
|
|
|
# Instructions are in day1_input.txt. |
|
|
|
|
|
|
|
# I'm gonna overengineer this. |
|
|
|
|
|
|
|
import argparse |
|
|
|
import logging |
|
|
|
import os |
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
sh = logging.StreamHandler() |
|
|
|
logger.addHandler(sh) |
|
|
|
logger.setLevel(logging.INFO) |
|
|
|
|
|
|
|
DEFAULTS = { |
|
|
|
"starting_position": 50, |
|
|
|
"num_positions": 100, |
|
|
|
"filename": "day1_input.txt" |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class RotaryLock: |
|
|
|
def __init__(self, starting_position:int|None=None, num_positions:int|None=None): |
|
|
|
try: |
|
|
|
num_positions = int(num_positions) if num_positions is not None else DEFAULTS["num_positions"] |
|
|
|
except ValueError: |
|
|
|
logger.warning(f"Number of positions {num_positions} is not an integer. Setting to default ({DEFAULTS['num_positions']}).") |
|
|
|
num_positions = DEFAULTS["num_positions"] |
|
|
|
if num_positions <= 0: |
|
|
|
logger.warning(f"Number of positions {num_positions} is not positive. Setting to default ({DEFAULTS['num_positions']}).") |
|
|
|
num_positions = DEFAULTS["num_positions"] |
|
|
|
|
|
|
|
self.last_position = num_positions - 1 |
|
|
|
self.first_position = 0 |
|
|
|
|
|
|
|
try: |
|
|
|
starting_position = int(starting_position) if starting_position is not None else DEFAULTS["starting_position"] |
|
|
|
except ValueError: |
|
|
|
logger.warning(f"Starting position {starting_position} is not an integer. Setting to default ({DEFAULTS['starting_position']}).") |
|
|
|
starting_position = DEFAULTS["starting_position"] |
|
|
|
if self.first_position <= int(starting_position) <= self.last_position: |
|
|
|
self.current_position = (starting_position) |
|
|
|
else: |
|
|
|
logger.warning( |
|
|
|
f"Starting position {starting_position} falls outside of possible positions {self.first_position}-{self.last_position}. Setting to 0." |
|
|
|
) |
|
|
|
self.current_position = 0 |
|
|
|
self.position_history = [] |
|
|
|
self.position_history.append(self.current_position) |
|
|
|
self.instructions = [] |
|
|
|
self.num_zeroes = 0 |
|
|
|
logger.debug(f"RotaryLock created!") |
|
|
|
logger.debug(self.__dict__) |
|
|
|
|
|
|
|
def read_instructions(self, filename:str|None=None) -> None: |
|
|
|
filename = filename if filename is not None else DEFAULTS["filename"] |
|
|
|
if not filename.startswith("/"): |
|
|
|
full_path = os.path.join(os.curdir, filename) |
|
|
|
else: |
|
|
|
full_path = filename |
|
|
|
if os.path.exists(full_path) and os.path.isfile(full_path): |
|
|
|
with open(full_path, "r") as f: |
|
|
|
lines = f.readlines() |
|
|
|
elif not os.path.exists(full_path): |
|
|
|
logger.error(f"File {full_path} does not exist.") |
|
|
|
return |
|
|
|
elif not os.path.isfile(full_path): |
|
|
|
logger.error(f"File {full_path} is not a file.") |
|
|
|
return |
|
|
|
self.instructions = [l.strip() for l in lines] |
|
|
|
|
|
|
|
def rotate_right(self) -> None: |
|
|
|
if self.current_position == self.last_position: |
|
|
|
self.current_position = self.first_position |
|
|
|
else: |
|
|
|
self.current_position += 1 |
|
|
|
|
|
|
|
def rotate_left(self) -> None: |
|
|
|
if self.current_position == self.first_position: |
|
|
|
self.current_position = self.last_position |
|
|
|
else: |
|
|
|
self.current_position -= 1 |
|
|
|
|
|
|
|
def rotate_lock(self, direction:str, magnitude:int) -> None: |
|
|
|
try: |
|
|
|
d = direction.lower() |
|
|
|
except AttributeError: |
|
|
|
logger.error(f"Supplied direction {direction} is not usable (must be a string).") |
|
|
|
return |
|
|
|
if d not in ("l","r"): |
|
|
|
logger.error(f"Supplied direction {direction} is not usable (must be L or R).") |
|
|
|
return |
|
|
|
try: |
|
|
|
n = int(magnitude) |
|
|
|
except ValueError: |
|
|
|
logger.error(f"Supplied magnitude {magnitude} is not an integer.") |
|
|
|
return |
|
|
|
for i in range(n): |
|
|
|
match d: |
|
|
|
case "r": |
|
|
|
self.rotate_right() |
|
|
|
case "l": |
|
|
|
self.rotate_left() |
|
|
|
case default: |
|
|
|
logger.error(f"Direction {d} was not recognized. (How did we get here?)") |
|
|
|
if self.current_position == self.first_position: |
|
|
|
logger.debug(f"Saw a zero.") |
|
|
|
self.num_zeroes += 1 |
|
|
|
# if d == "r": |
|
|
|
# new_pos = self.current_position + n |
|
|
|
# if new_pos > self.last_position: |
|
|
|
# overflow = new_pos - (self.last_position + 1) |
|
|
|
# new_pos = overflow |
|
|
|
# else: |
|
|
|
# new_pos = self.current_position - n |
|
|
|
# if new_pos < 0: |
|
|
|
# overflow = n - (self.current_position + 1) |
|
|
|
# new_pos = self.last_position - overflow |
|
|
|
# self.current_position = new_pos |
|
|
|
logger.debug(f"After rotating {d.upper()} {n} places, current position is {self.current_position}.") |
|
|
|
|
|
|
|
def follow_instruction(self, instruction:str) -> None: |
|
|
|
try: |
|
|
|
d, n = instruction[0].lower(), int(instruction[1:]) |
|
|
|
except (AttributeError, ValueError, IndexError): |
|
|
|
logger.error(f"Instruction {instruction} is unreadable (format must be DN+).") |
|
|
|
return |
|
|
|
logger.debug(f"Following instruction to rotate {d} by {n} positions.") |
|
|
|
self.rotate_lock(direction=d, magnitude=n) |
|
|
|
# if self.current_position == 0: |
|
|
|
# logger.debug(f"Landed on 0.") |
|
|
|
# self.num_zeroes += 1 |
|
|
|
|
|
|
|
def follow_instructions(self, instruction_list:list[str]|None=None) -> None: |
|
|
|
if instruction_list is None: |
|
|
|
self.read_instructions() |
|
|
|
instruction_list = self.instructions |
|
|
|
else: |
|
|
|
if len(self.instructions) == 0: |
|
|
|
logger.error(f"Instructions were not passed and have not been initialized.") |
|
|
|
return |
|
|
|
instruction_list = self.instructions |
|
|
|
try: |
|
|
|
instructions = [x for x in instruction_list] |
|
|
|
except TypeError: |
|
|
|
logger.error(f"Instruction list {instruction_list} is not an iterable.") |
|
|
|
tenths = len(instructions) // 10 |
|
|
|
logger.debug(f"Processing {len(instructions)} instructions.") |
|
|
|
logger.debug(f"Reporting every {tenths} instructions.") |
|
|
|
for i, instr in enumerate(instructions): |
|
|
|
if tenths > 0 and i % tenths == 0: |
|
|
|
logger.debug(f"Processing instruction {i}.") |
|
|
|
self.follow_instruction(instr) |
|
|
|
logger.info(f"Completed all instructions.") |
|
|
|
logger.info(f"Final position is {self.current_position}.") |
|
|
|
logger.info(f"Number of zeroes is {self.num_zeroes}.") |
|
|
|
|
|
|
|
|
|
|
|
def process_instructions(filename:str, start:int, num_pos:int): |
|
|
|
rlock = RotaryLock(starting_position=start, num_positions=num_pos) |
|
|
|
rlock.read_instructions(filename) |
|
|
|
rlock.follow_instructions() |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
parser = argparse.ArgumentParser() |
|
|
|
parser.add_argument("--filename", type=str, default=DEFAULTS["filename"], help="Full or local path to instruction file") |
|
|
|
parser.add_argument("--start", type=int, default=DEFAULTS["starting_position"], help="Starting position on the dial") |
|
|
|
parser.add_argument("--num_pos", type=int, default=DEFAULTS["num_positions"], help="Number of positions on the dial") |
|
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging") |
|
|
|
|
|
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
if args.verbose: |
|
|
|
logger.setLevel(logging.DEBUG) |
|
|
|
|
|
|
|
process_instructions(args.filename, args.start, args.num_pos) |