2023-03-14 18:02:48 +01:00
|
|
|
#!/usr/bin/env pypy3
|
2023-03-05 15:23:59 +01:00
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
from scrapper import AdventSession
|
|
|
|
from getpass import getpass
|
2023-03-04 15:03:21 +01:00
|
|
|
import importlib
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import pathlib
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
YEAR = 2019
|
2023-03-04 15:03:21 +01:00
|
|
|
PATH = pathlib.Path(__file__).parent
|
2023-03-05 15:23:59 +01:00
|
|
|
INPUT_PATH = PATH / 'input'
|
2023-03-04 15:03:21 +01:00
|
|
|
|
|
|
|
class Tests(list):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""
|
|
|
|
Test cases class. A test case is a tuple of the form
|
|
|
|
(puzzle_input, partI_result, partII_result).
|
|
|
|
"""
|
|
|
|
|
2023-03-04 15:03:21 +01:00
|
|
|
def add(self, test_input, partI=None, partII=None):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""Add a test case"""
|
2023-03-04 15:03:21 +01:00
|
|
|
self.append((str(test_input), partI, partII))
|
|
|
|
|
|
|
|
def testf(tests, f, part):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""
|
|
|
|
Tests a function f on test cases tests and compares the result
|
|
|
|
to test_case[part]. The test is not run if test_case[part] == None.
|
|
|
|
"""
|
2023-03-04 15:03:21 +01:00
|
|
|
tests = [(t[0], t[part]) for t in tests if t[part] != None]
|
|
|
|
if tests == []:
|
|
|
|
return "No tests"
|
|
|
|
for i, (case, sol) in enumerate(tests):
|
|
|
|
if sol == None:
|
|
|
|
continue
|
|
|
|
r = f(case)
|
|
|
|
if r == sol:
|
|
|
|
continue
|
|
|
|
return "Failed on test {}. Expected {}. Got {}".format(i+1, sol, r)
|
|
|
|
return "OK"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Solver:
|
2023-03-04 15:20:58 +01:00
|
|
|
"""
|
|
|
|
Solver class. A solver is a module with functions partI and partII,
|
|
|
|
an optional preprocessing function preproc, and an optional variable
|
|
|
|
tests of type Tests.
|
|
|
|
"""
|
|
|
|
|
2023-03-04 15:03:21 +01:00
|
|
|
def __init__(self, mod):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""Initilizes the solver instance of module mod."""
|
2023-03-04 15:03:21 +01:00
|
|
|
def unpack(name, default):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""Returns mod.name if it exists and default otherwise."""
|
2023-03-04 15:03:21 +01:00
|
|
|
return getattr(mod, name) if name in dir(mod) else default
|
|
|
|
|
|
|
|
dn_pattern = re.compile("\d\d?")
|
|
|
|
self.preproc = unpack('preproc', NotImplemented)
|
|
|
|
self.tests = unpack('tests', [])
|
|
|
|
self.partI = getattr(mod, 'partI')
|
|
|
|
self.partII = getattr(mod, 'partII')
|
|
|
|
|
2023-03-04 15:20:58 +01:00
|
|
|
|
2023-03-04 15:03:21 +01:00
|
|
|
def solve(self, puzzle_input):
|
2023-03-05 15:23:59 +01:00
|
|
|
"""Solves puzzle input and returns pair of solutions"""
|
2023-03-04 15:03:21 +01:00
|
|
|
puzzle_input = puzzle_input.rstrip()
|
|
|
|
if self.preproc != NotImplemented:
|
|
|
|
puzzle_input = self.preproc(puzzle_input)
|
|
|
|
ptI = self.partI(puzzle_input)
|
|
|
|
ptII = self.partII(puzzle_input)
|
|
|
|
return ptI, ptII
|
|
|
|
|
|
|
|
def test(self):
|
2023-03-05 15:23:59 +01:00
|
|
|
"""Tests the solver and returns pair of test results."""
|
2023-03-04 15:03:21 +01:00
|
|
|
tests = self.tests
|
|
|
|
if self.preproc != NotImplemented:
|
|
|
|
tests = [(self.preproc(str(t)), s1, s2)
|
|
|
|
for (t, s1, s2) in tests]
|
|
|
|
ptI = testf(tests, self.partI, 1)
|
|
|
|
ptII = testf(tests, self.partII, 2)
|
|
|
|
return ptI, ptII
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-03-05 15:23:59 +01:00
|
|
|
return r
|
2023-03-04 15:03:21 +01:00
|
|
|
|
|
|
|
|
|
|
|
parser = ArgumentParser()
|
|
|
|
|
|
|
|
parser.description = \
|
2023-03-13 23:55:12 +01:00
|
|
|
"""Solves Advent of Code 2019 puzzles.
|
|
|
|
The puzzles are available at https://adventofcode.com/2019/"""
|
2023-03-04 15:03:21 +01:00
|
|
|
|
|
|
|
parser.add_argument('-d', '--day',
|
|
|
|
action='store',
|
|
|
|
type=str,
|
2023-03-05 15:23:59 +01:00
|
|
|
metavar='DAY[-DAY]',
|
|
|
|
help='solve puzzle of day DAY or of range DAY-DAY')
|
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
group = parser.add_mutually_exclusive_group()
|
|
|
|
group.add_argument('-i', '--input',
|
2023-03-05 15:23:59 +01:00
|
|
|
action='store',
|
|
|
|
type=str,
|
|
|
|
metavar='INPUT',
|
|
|
|
default=INPUT_PATH,
|
|
|
|
help='use INPUT as puzzle input')
|
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
group.add_argument('-t', '--test',
|
2023-03-04 15:03:21 +01:00
|
|
|
action='store_true',
|
|
|
|
help="run tests")
|
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
group.add_argument('--fetch-session',
|
|
|
|
action='store_true',
|
|
|
|
help='fetch puzzle inputs and solutions for session key')
|
|
|
|
|
2023-03-05 15:23:59 +01:00
|
|
|
def parse_day_arg(s, mind, maxd):
|
2023-03-04 15:20:58 +01:00
|
|
|
"""Parses day argument. Returns a list of days."""
|
2023-03-04 15:03:21 +01:00
|
|
|
split = s.split('-')
|
|
|
|
if len(split) == 1:
|
|
|
|
return [int(split[0])]
|
|
|
|
elif len(split) == 2:
|
|
|
|
d, D = map(int, split)
|
2023-03-05 15:23:59 +01:00
|
|
|
return list(range(max(1, d), min(D+1, 26)))
|
2023-03-04 15:03:21 +01:00
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
|
2023-03-05 15:23:59 +01:00
|
|
|
def main():
|
2023-03-04 15:03:21 +01:00
|
|
|
solver_pattern = re.compile("day(\d\d?).py")
|
|
|
|
matches = map(solver_pattern.fullmatch, os.listdir(PATH))
|
|
|
|
solvers = {int(s.group(1)) : s.group(0) for s in matches if s != None}
|
2023-03-05 15:23:59 +01:00
|
|
|
|
|
|
|
mind, maxd = min(solvers), max(solvers)
|
|
|
|
days = sorted(solvers.keys())
|
2023-03-04 15:03:21 +01:00
|
|
|
|
|
|
|
args = parser.parse_args()
|
2023-03-05 15:23:59 +01:00
|
|
|
#Handle day argument
|
2023-03-04 15:03:21 +01:00
|
|
|
if args.day != None:
|
|
|
|
try:
|
2023-03-05 15:23:59 +01:00
|
|
|
days = parse_day_arg(args.day, mind, maxd)
|
2023-03-04 15:03:21 +01:00
|
|
|
except ValueError:
|
2023-03-05 15:23:59 +01:00
|
|
|
return 'invalid format of argument -d/--day'
|
|
|
|
|
|
|
|
#Handle input argument
|
|
|
|
inputs = {}
|
|
|
|
input_path = pathlib.Path('.')
|
|
|
|
num_pattern = re.compile(r'\d\d?')
|
|
|
|
try:
|
|
|
|
fs = os.listdir(args.input)
|
|
|
|
input_path /= args.input
|
|
|
|
for fn in fs:
|
|
|
|
ns = num_pattern.findall(fn)
|
|
|
|
if ns == []:
|
|
|
|
print('warning : ignoring input file {}'.format(fn))
|
|
|
|
continue
|
|
|
|
n = int(ns[0])
|
|
|
|
inputs[n] = fn
|
|
|
|
except FileNotFoundError:
|
|
|
|
return "input directory or file doesn't exist"
|
|
|
|
except NotADirectoryError:
|
|
|
|
if len(days) > 1:
|
|
|
|
return "input is file"
|
|
|
|
day = days[0]
|
|
|
|
inputs[day] = args.input
|
|
|
|
|
2023-03-14 18:02:48 +01:00
|
|
|
#Fetch inputs of session
|
|
|
|
if args.fetch_session:
|
|
|
|
answer = input('Fetch puzzle input and solutions for session key?\n' +
|
|
|
|
'The session key will not be stored. [Y/n]: ')
|
|
|
|
if 'n' in answer.lower():
|
|
|
|
return
|
|
|
|
key = getpass('Session key: ')
|
|
|
|
sess = AdventSession(key)
|
|
|
|
anon_pattern = re.compile(r'\(anonymous user #(\d+)\)')
|
|
|
|
user = sess.get_user()
|
|
|
|
print('Fetching inputs for {}'.format(user))
|
|
|
|
|
|
|
|
anon_match = anon_pattern.fullmatch(user)
|
|
|
|
if anon_match == None:
|
|
|
|
dir_name = 'user_{}'.format(user.replace(' ', '-'))
|
|
|
|
else:
|
|
|
|
dir_name = 'anon_{}'.format(anon_match.group(1))
|
|
|
|
|
|
|
|
if not os.path.exists(dir_name):
|
|
|
|
os.makedirs(dir_name)
|
|
|
|
mssg = 'Choose directory name or press ENTER for [{}]: '.format(dir_name)
|
|
|
|
r = input(mssg)
|
|
|
|
if r != '':
|
|
|
|
dir_name = r
|
|
|
|
path = PATH / pathlib.Path(dir_name)
|
|
|
|
sol_fn = 'solutions'
|
|
|
|
#Create / overwrite solution file
|
|
|
|
open(path / sol_fn, 'a').close()
|
|
|
|
with open(path / sol_fn, 'w') as f:
|
|
|
|
f.write('')
|
|
|
|
|
|
|
|
input_format = 'day{0:0>2}.txt'
|
|
|
|
for d in days:
|
|
|
|
print('[{}{}] {}/25'.format('#'*d, '.'*(25-d), d), end='\r')
|
|
|
|
try:
|
|
|
|
puzzle_input = sess.get_input(YEAR, d)
|
|
|
|
except e:
|
|
|
|
print('warning : failed to fetch input for day {}'.format(d))
|
|
|
|
print(e)
|
|
|
|
continue
|
|
|
|
#Create file if it doesn't exist
|
|
|
|
fn = path / input_format.format(d)
|
|
|
|
open(fn, 'a').close()
|
|
|
|
with open(fn, 'w') as f:
|
|
|
|
f.write(puzzle_input)
|
|
|
|
|
|
|
|
try:
|
|
|
|
solutions = sess.get_solutions(YEAR, d)
|
|
|
|
except e:
|
|
|
|
print('warning : failed to fetch solutions for day {}'.format(d))
|
|
|
|
print(e)
|
|
|
|
continue
|
|
|
|
|
|
|
|
with open(path / sol_fn, 'a') as f:
|
|
|
|
f.write(' '.join(solutions) + '\n')
|
|
|
|
|
|
|
|
print('\nDone')
|
|
|
|
return
|
|
|
|
|
2023-03-05 15:23:59 +01:00
|
|
|
#Solve days
|
2023-03-04 15:03:21 +01:00
|
|
|
for d in days:
|
|
|
|
print("=" * 6 + " Day {} ".format(d) + "=" * 6)
|
|
|
|
if d not in solvers:
|
2023-03-05 15:23:59 +01:00
|
|
|
return "no solver for day {}".format(d)
|
2023-03-04 15:03:21 +01:00
|
|
|
mod_name = solvers[d][:-3]
|
|
|
|
try:
|
|
|
|
mod = importlib.import_module(mod_name)
|
|
|
|
solver = Solver(mod)
|
|
|
|
except:
|
2023-03-05 15:23:59 +01:00
|
|
|
return "bad solver\n\n{}".format(traceback.format_exc())
|
2023-03-04 15:03:21 +01:00
|
|
|
try:
|
|
|
|
if args.test:
|
2023-03-05 15:23:59 +01:00
|
|
|
ptI, ptII = solver.test()
|
2023-03-04 15:03:21 +01:00
|
|
|
else:
|
2023-03-05 15:23:59 +01:00
|
|
|
if d not in inputs:
|
|
|
|
return 'no input file'
|
|
|
|
with open(input_path / inputs[d]) as f:
|
|
|
|
puzzle_input = f.read()
|
|
|
|
ptI, ptII = solver.solve(puzzle_input)
|
|
|
|
print("Part One : {}".format(ptI))
|
|
|
|
print("Part Two : {}".format(ptII))
|
2023-03-04 15:03:21 +01:00
|
|
|
except:
|
2023-03-05 15:23:59 +01:00
|
|
|
return "error in solver\n\n{}".format(traceback.format_exc())
|
2023-03-04 15:03:21 +01:00
|
|
|
|
2023-03-05 15:23:59 +01:00
|
|
|
if __name__ == '__main__':
|
|
|
|
error = main()
|
|
|
|
if error != None:
|
|
|
|
print('error : {}'.format(error))
|