Last active
December 23, 2015 18:14
-
-
Save mjpieters/1566689dc72ab4055a40 to your computer and use it in GitHub Desktop.
Advent of Code fun
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Having fun with http://adventofcode.com/day/6 Advent of Code | |
import re | |
from itertools import product | |
from collections import defaultdict | |
try: | |
from PIL import Image | |
except ImportError: | |
Image = None | |
class Grid(object): | |
_line_pat = re.compile( | |
r'(?:(?:turn (on|off))|(toggle)) (\d+),(\d+) through (\d+),(\d+)') | |
def __init__(self): | |
self._lights = set() | |
def __len__(self): | |
return len(self._lights) | |
def _gen_grid(self, x1, y1, x2, y2): | |
return product(range(x1, x2 + 1), range(y1, y2 + 1)) | |
def process_line(self, line): | |
match = self._line_pat.search(line) | |
if not match: | |
return | |
state, toggle = match.group(1, 2) | |
x1, y1, x2, y2 = map(int, match.group(3, 4, 5, 6)) | |
if state == 'on': | |
self._lights.update(self._gen_grid(x1, y1, x2, y2)) | |
elif state == 'off': | |
self._lights.difference_update(self._gen_grid(x1, y1, x2, y2)) | |
else: | |
grid = set(self._gen_grid(x1, y1, x2, y2)) | |
switch_on = grid - self._lights | |
self._lights -= grid & self._lights | |
self._lights |= switch_on | |
# tests | |
g = Grid() | |
g.process_line('turn on 0,0 through 999,999') | |
assert len(g) == 1000000 | |
g = Grid() | |
g.process_line('toggle 0,0 through 999,0') | |
assert len(g) == 1000 | |
g = Grid() | |
g.process_line('turn on 0,0 through 499,0') | |
g.process_line('toggle 0,0 through 999,0') | |
assert len(g) == 500 | |
g = Grid() | |
g.process_line('turn on 499,0 through 500,999') | |
g.process_line('turn off 499,499 through 500,500') | |
assert len(g) == (2000 - 4) | |
class BrightnessGrid(Grid): | |
def __init__(self): | |
self._lights = [[0 for _ in range(1000)] for _ in range(1000)] | |
self._brightness = 0 | |
def __len__(self): | |
return self._brightness | |
def process_line(self, line): | |
match = self._line_pat.search(line) | |
if not match: | |
return | |
state, toggle = match.group(1, 2) | |
x1, y1, x2, y2 = map(int, match.group(3, 4, 5, 6)) | |
lights = self._lights | |
coords = self._gen_grid(x1, y1, x2, y2) | |
if state == 'on' or toggle: | |
amount = 2 if toggle else 1 | |
for x, y in coords: | |
lights[x][y] += amount | |
self._brightness += amount | |
else: | |
for x, y in coords: | |
if lights[x][y] > 0: | |
lights[x][y] -= 1 | |
self._brightness -= 1 | |
g = BrightnessGrid() | |
g.process_line('turn on 0,0 through 0,0') | |
assert len(g) == 1 | |
g.process_line('toggle 0,0 through 999,999') | |
assert len(g) == 2000001 | |
g.process_line('turn off 0,0 through 999,999') | |
assert len(g) == 1000001 | |
g.process_line('turn off 0,0 through 999,999') | |
assert len(g) == 1 | |
g.process_line('turn off 0,0 through 999,999') | |
assert len(g) == 0 | |
if Image is not None: | |
class ImageGrid(Grid): | |
def __init__(self): | |
self._lights = Image.new('L', (1000, 1000), 0) | |
def process_line(self, line): | |
match = self._line_pat.search(line) | |
if not match: | |
return | |
state, toggle = match.group(1, 2) | |
box = [int(c) for c in match.group(3, 4, 5, 6)] | |
lights = self._lights | |
region = lights.crop(box) | |
if state == 'on': | |
region = region.point(lambda p: 255) | |
elif state == 'off': | |
region = region.point(lambda p: 0) | |
else: | |
region = region.point(lambda p: 255 - p) | |
lights.paste(region, box) | |
def save(self, filename): | |
self._lights.save(filename) | |
class BrightnessImageGrid(ImageGrid, BrightnessGrid): | |
def process_line(self, line): | |
match = self._line_pat.search(line) | |
if not match: | |
return | |
state, toggle = match.group(1, 2) | |
box = [int(c) for c in match.group(3, 4, 5, 6)] | |
lights = self._lights | |
region = lights.crop(box) | |
if state == 'on' or toggle: | |
amount = 10 if toggle else 5 | |
region = region.point(lambda p: p + amount) | |
else: | |
region = region.point(lambda p: max(0, p - 5)) | |
lights.paste(region, box) | |
if __name__ == '__main__': | |
import sys | |
import os.path | |
filename = sys.argv[-1] | |
if '-1' in sys.argv: | |
g = Grid() | |
with open(filename) as f: | |
for line in f: | |
g.process_line(line) | |
print('First part:', len(g)) | |
if '-2' in sys.argv: | |
bg = BrightnessGrid() | |
with open(filename) as f: | |
for line in f: | |
bg.process_line(line) | |
print('Second part:', len(bg)) | |
if Image is not None: | |
if '-img1' in sys.argv: | |
output = os.path.join( | |
os.path.dirname(filename), | |
os.path.splitext(os.path.basename(filename))[0] + '_img1.png') | |
ig = ImageGrid() | |
with open(filename) as f: | |
for line in f: | |
ig.process_line(line) | |
ig.save(output) | |
print('Saved', output) | |
if '-img2' in sys.argv: | |
output = os.path.join( | |
os.path.dirname(filename), | |
os.path.splitext(os.path.basename(filename))[0] + '_img2.png') | |
big = BrightnessImageGrid() | |
with open(filename) as f: | |
for line in f: | |
big.process_line(line) | |
big.save(output) | |
print('Saved', output) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import operator | |
import re | |
class Expression(object): | |
_ops = { | |
'AND': operator.and_, | |
'OR': operator.or_, | |
'NOT': lambda x, y: ~y & 0xFFFF, # bitwise inversion, only one operand | |
'LSHIFT': lambda x, y: (x << y) & 0xFFFF, | |
'RSHIFT': operator.rshift, | |
None: lambda x, y: x, # direct assignment, only one operand | |
} | |
_line = re.compile( | |
r'(?:(?P<op1>[a-z]+|\d+) )?(?:(?P<operator>{}) (?P<op2>[a-z]+|\d+) )?' | |
r'-> (?P<target>[a-z]+)'.format( | |
'|'.join([k for k in _ops if k is not None]))) | |
def __init__(self, op1, operator, op2, target): | |
self.op1 = int(op1) if op1 and op1.isdigit() else op1 | |
self.op2 = int(op2) if op2 and op2.isdigit() else op2 | |
self.op_name = operator | |
self.operator = self._ops[operator] | |
self.target = target | |
@classmethod | |
def from_line(cls, line): | |
match = cls._line.search(line) | |
if not match: | |
raise ValueError('not a valid expression') | |
return cls(**match.groupdict()) | |
def __repr__(self): | |
return ( | |
'{}("{s.op1!r} {s.op_name!r} {s.op2!r} -> {s.target!r}")'.format( | |
type(self).__name__, s=self)) | |
def __call__(self, expressions, _cache): | |
if self.target in _cache: | |
return _cache[self.target] | |
op1, op2 = ( | |
expressions[op](expressions, _cache) if isinstance(op, str) else op | |
for op in (self.op1, self.op2)) | |
res = _cache[self.target] = self.operator(op1, op2) | |
return res | |
class Solver(object): | |
def __init__(self): | |
# target -> expression. All targets are unique | |
self.expressions = {} | |
def parse_instruction(self, line): | |
expr = Expression.from_line(line) | |
self.expressions[expr.target] = expr | |
def solve(self, target='a'): | |
return self.expressions[target](self.expressions, {}) | |
def to_dot_graph(self, output): | |
with open(output, 'w') as df: | |
df.write('digraph advent_expression {\n') | |
for node in self.expressions.values(): | |
df.write( | |
'n{0} [shape=box,label="{0}"]\n' | |
'n{0}_expr [shape=plaintext,label="{1}"]\n' | |
'n{0} -> n{0}_expr\n'.format( | |
node.target, node.op_name or '=')) | |
for label, op in (('l', node.op1), ('r', node.op2)): | |
if op is None: | |
continue | |
if isinstance(op, int): | |
# Numbers are repeated | |
df.write( | |
'n{0}{1} [shape=circle,label="{1}"]\n' | |
'n{0}_expr -> n{0}{1} [label="{2}"]\n'.format( | |
node.target, op, label)) | |
else: | |
df.write('n{0}_expr -> n{1} [label="{2}"]\n'.format( | |
node.target, op, label)) | |
df.write('}\n') | |
if __name__ == '__main__': | |
import sys | |
import os.path | |
filename = sys.argv[-1] | |
solver = Solver() | |
with open(filename) as inputfile: | |
for line in inputfile: | |
solver.parse_instruction(line) | |
result = solver.solve() | |
print('Part 1:', result) | |
if '--graph' in sys.argv: | |
dirname, base = os.path.split(filename) | |
output = os.path.join( | |
dirname, os.path.splitext(base)[0] + '.dot') | |
solver.to_dot_graph(output) | |
print('Saved graph to', output) | |
solver.expressions['b'].op1 = result | |
print('Part 2:', solver.solve()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from ast import literal_eval | |
here = os.path.dirname(os.path.abspath(__file__)) | |
filename = os.path.join(here, 'inputs/input8.txt') | |
# quick and dirty way; have Python do the parsing | |
diff1 = diff2 = 0 | |
for l in open(filename): | |
diff1 += len(l) - len(literal_eval(l)) | |
diff2 += 2 + l.count('"') + l.count('\\') | |
print('Part 1:', diff1) | |
print('Part 2:', diff2) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Travelling salesm^WSanta. | |
# Just bruteforce it; it's only 8! comparisons | |
import re | |
from itertools import permutations | |
def shortest(g): | |
return min( | |
sum(g[a][b] for a, b in zip(path, path[1:])) | |
for path in permutations(g)) | |
def longest(g): | |
return max( | |
sum(g[a][b] for a, b in zip(path, path[1:])) | |
for path in permutations(g)) | |
def read_graph(fileobj): | |
parse_line = re.compile(r'(\w+) to (\w+) = (\d+)') | |
g = {} | |
for line in fileobj: | |
m = parse_line.search(line) | |
if not m: | |
continue | |
from_, to, dist = m.groups() | |
g.setdefault(from_, {})[to] = int(dist) | |
g.setdefault(to, {})[from_] = int(dist) | |
return g | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[1] | |
with open(filename) as f: | |
g = read_graph(f) | |
print('Part 1:', shortest(g)) | |
print('Part 2:', longest(g)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# --- Day 10: Elves Look, Elves Say --- | |
from itertools import groupby | |
from functools import reduce | |
def looksay(digits): | |
return ''.join(['{}{}'.format(len(list(group)), d) | |
for d, group in groupby(digits)]) | |
assert looksay('1') == '11' | |
assert looksay('11') == '21' | |
assert looksay('21') == '1211' | |
assert looksay('1211') == '111221' | |
assert looksay('111221') == '312211' | |
if __name__ == '__main__': | |
res1 = reduce(lambda prev, i: looksay(prev), range(40), '1321131112') | |
print('Part 1:', len(res1)) | |
# iterate an *additional* 10 times | |
res2 = reduce(lambda prev, i: looksay(prev), range(10), res1) | |
print('Part 2:', len(res2)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from string import ascii_lowercase | |
from itertools import chain, permutations, product | |
import bisect | |
import re | |
import time | |
letters = ''.join([l for l in ascii_lowercase if l not in 'ilo']) | |
# 21 valid 3-letter consecutive sequences | |
sequences = [''.join(seq) | |
for seq in zip( | |
ascii_lowercase, ascii_lowercase[1:], ascii_lowercase[2:]) | |
if not any(c in 'ilo' for c in seq)] | |
def generate_next(password): | |
# no z, this is deliberate! The KeyError signals we need to recurse | |
next_map = {a: b for a, b in zip(letters, letters[1:])} | |
# Technically speaking, we would need to account for i, l and o too, | |
# since Santa may have used those in the old password | |
next_map.update({'i': 'j', 'l': 'm', 'o': 'p'}) | |
def increment(pw): | |
try: | |
return pw[:-1] + next_map[pw[-1]] | |
except KeyError: | |
return increment(pw[:-1]) + 'a' | |
has_seq = re.compile(r'({})'.format('|'.join(sequences))) | |
has_doubles = re.compile(r'(.)\1.*(.)\2') | |
def isvalid(pw): | |
return bool(has_seq.search(pw) and has_doubles.search(pw)) | |
while True: | |
password = increment(password) | |
if isvalid(password): | |
return password | |
def generate_all(): | |
# There are only 15 ^ 5 possible 3-character consequtive sequences in the | |
# passwords. Combined with the requirement there are 2 doubled letter | |
# pairs, limits the total to 6712592 (6.7 million) different passwords | |
doubled = [l + l for l in letters] | |
print('Generating all possible passwords') | |
start = time.time() | |
all_passwords = sorted(set(chain( | |
# no overlap between sequence and doubled letters | |
(''.join(c) | |
for p in product(sequences, letters, doubled, doubled) | |
for c in permutations(p)), | |
# last letter of sequence doubled | |
(''.join(c) | |
for p in product((s + s[-1] for s in sequences), | |
doubled, *[letters] * 2) | |
for c in permutations(p)), | |
# first letter of sequence doubled | |
(''.join(c) | |
for p in product((s[0] + s for s in sequences), | |
doubled, *[letters] * 2) | |
for c in permutations(p)), | |
# first and last letter of sequence doubled | |
(''.join(c) | |
for p in product((s[0] + s + s[-1] for s in sequences), | |
*[letters] * 3) | |
for c in permutations(p)), | |
))) | |
print('Done generating all {} possible passwords in {:.3f} seconds'.format( | |
len(all_passwords), time.time() - start)) | |
return all_passwords | |
if __name__ == '__main__': | |
import sys | |
old_password = sys.argv[-1] | |
if '--gen' in sys.argv: | |
all_passwords = generate_all() | |
new_password = all_passwords[ | |
bisect.bisect_right(all_passwords, old_password)] | |
print('Part 1:', new_password) | |
next_password = all_passwords[ | |
bisect.bisect_right(all_passwords, new_password)] | |
print('Part 2:', next_password) | |
else: | |
new_password = generate_next(old_password) | |
print('Part 1:', new_password) | |
next_password = generate_next(new_password) | |
print('Part 2:', next_password) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from itertools import islice, permutations | |
import re | |
def circular_window(seq, n=2): | |
it = iter(seq + seq[:n - 1]) | |
result = tuple(islice(it, n)) | |
yield result | |
for elem in it: | |
result = result[1:] + (elem,) | |
yield result | |
def calculate_happiness_change(graph, order): | |
return sum(graph[m][l] + graph[m][r] | |
for l, m, r in circular_window(order, 3)) | |
def find_max_happiness_change(graph): | |
return max(calculate_happiness_change(graph, o) | |
for o in permutations(graph)) | |
def read_input(fileobj): | |
linepattern = re.compile( | |
r'(\w+) would (gain|lose) (\d+) happiness units ' | |
r'by sitting next to (\w+).') | |
graph = {} | |
for line in fileobj: | |
match = linepattern.search(line) | |
if not match: | |
continue | |
name1, direction, change, name2 = match.groups() | |
change = int(change) if direction == 'gain' else -int(change) | |
graph.setdefault(name1, {})[name2] = change | |
return graph | |
if __name__ == '__main__': | |
import os.path | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as inf: | |
graph = read_input(inf) | |
print('Part 1:', find_max_happiness_change(graph)) | |
for name in graph: | |
graph[name]['Myself'] = 0 | |
graph['Myself'] = dict.fromkeys(graph, 0) | |
print('Part 2:', find_max_happiness_change(graph)) | |
if '--graph' in sys.argv: | |
dirname, basename = os.path.split(filename) | |
output = os.path.join(dirname, os.path.splitext(basename)[0] + '.dot') | |
order = max((o for o in permutations(graph)), | |
key=lambda o: calculate_happiness_change(graph, o)) | |
with open(output, 'w') as df: | |
df.write('graph advent_seating {\nlayout="circo";\n') | |
for name, toright in circular_window(order, 2): | |
df.write( | |
'n{0} [shape=circle, label="{0}", width=1]\n' | |
'n{0} -- n{1} ' | |
'[headlabel="{2}", taillabel="{3}", ' | |
'labeldistance=2];\n'.format( | |
name, toright, | |
graph[name][toright], graph[toright][name])) | |
df.write('}\n') | |
print('Written graph to', output) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
class Reindeer(object): | |
def __init__(self, name, speed, travel_time, rest_time): | |
self.name = name | |
self.speed = speed | |
self.travel_time = travel_time | |
self.rest_time = rest_time | |
self.reset() | |
def __eq__(self, other): | |
if not isinstance(other, Reindeer): | |
return NotImplemented | |
return self.name == other.name | |
def __hash__(self): | |
return hash(self.name) | |
def distance_travelled(self, total_time): | |
periods = total_time // (self.travel_time + self.rest_time) | |
travelled = min( | |
total_time - (self.travel_time + self.rest_time) * periods, | |
self.travel_time) + periods * self.travel_time | |
return self.speed * travelled | |
def reset(self): | |
self.distance = 0 | |
self.score = 0 | |
self.running = True | |
self.duration = self.travel_time | |
def tick(self): | |
if self.running: | |
self.distance += self.speed | |
self.duration -= 1 | |
if self.duration == 0: | |
self.running = not self.running | |
self.duration = ( | |
self.travel_time if self.running else self.rest_time) | |
def run_race(reindeer, race_duration): | |
furthest_distance = 0 | |
reindeer_in_lead = set() | |
for second in range(race_duration): | |
for racing_reindeer in reindeer: | |
racing_reindeer.tick() | |
if racing_reindeer.distance > furthest_distance: | |
furthest_distance = racing_reindeer.distance | |
reindeer_in_lead = {racing_reindeer} | |
elif racing_reindeer.distance == furthest_distance: | |
reindeer_in_lead.add(racing_reindeer) | |
for leading_reindeer in reindeer_in_lead: | |
leading_reindeer.score += 1 | |
return max(r.score for r in reindeer) | |
def read_file(fileobj): | |
line_pattern = re.compile( | |
r'(\w+) can fly (\d+) km/s for (\d+) seconds, ' | |
r'but then must rest for (\d+) seconds.') | |
return [ | |
Reindeer(name, int(speed), int(tt), int(rt)) | |
for line in fileobj if line.strip() | |
for name, speed, tt, rt in (line_pattern.search(line).groups(),) | |
] | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-2] | |
race_duration = int(sys.argv[-1]) | |
with open(filename) as inf: | |
reindeer = read_file(inf) | |
print('Part 1:', max(r.distance_travelled(race_duration) | |
for r in reindeer)) | |
print('Part 2:', run_race(reindeer, race_duration)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import namedtuple | |
from functools import reduce | |
from itertools import product | |
from operator import mul | |
Ingredient = namedtuple( | |
'Ingredient', 'capacity durability flavor texture calories') | |
def read_ingredients(fileobj): | |
results = {} | |
for line in fileobj: | |
name, __, properties = line.strip().partition(': ') | |
properties = {name: int(val) | |
for prop in properties.split(',') | |
for name, val in (prop.split(),)} | |
results[name] = Ingredient(**properties) | |
return results | |
def optimise_for_score(ingredients, target_calories=None): | |
max_score = 0 | |
for combo in product(range(100), repeat=len(ingredients) - 1): | |
if sum(combo) > 100: | |
continue | |
combo += ((100 - sum(combo)),) | |
if target_calories is not None: | |
calories = sum(ingr.calories * factor | |
for factor, ingr in zip( | |
combo, ingredients.values())) | |
if calories != target_calories: | |
continue | |
scores = [sum(ingr[p] * factor | |
for factor, ingr in zip(combo, ingredients.values())) | |
for p in range(4)] | |
if any(s <= 0 for s in scores): | |
continue | |
score = reduce(mul, scores) | |
if score > max_score: | |
max_score = score | |
return max_score | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as ingrf: | |
ingr = read_ingredients(ingrf) | |
score = optimise_for_score(ingr) | |
print('Part 1:', score) | |
score = optimise_for_score(ingr, 500) | |
print('Part 2:', score) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
from itertools import takewhile | |
from random import shuffle | |
def one_step_substitute(molecule, replacements): | |
for i, atom in enumerate(molecule): | |
for subst in replacements.get(atom, []): | |
yield ''.join(molecule[:i] + [subst] + molecule[i + 1:]) | |
def random_replace(molecule, replacements): | |
# to hell with finding the right order of substitutions by analysis; | |
# there is only one solution and the *quickest* way to that solution | |
# is trying out random substition orderings. | |
molecule = ''.join(molecule) | |
reversed = [(v, k) for k in replacements for v in replacements[k]] | |
steps = 0 | |
target = molecule | |
while target != 'e': | |
changed = False | |
for repl, source in reversed: | |
if repl in target: | |
target = target.replace(repl, source, 1) | |
steps += 1 | |
changed = True | |
if not changed: | |
target = molecule | |
shuffle(reversed) | |
steps = 0 | |
return steps | |
def read_input(fileobj, _atom=re.compile('[A-Z][a-z]*')): | |
fileobj = iter(fileobj) | |
replacements = {} | |
for line in takewhile(lambda l: l.strip(), fileobj): | |
source, __, target = line.strip().partition(' => ') | |
replacements.setdefault(source, []).append(target) | |
molecule = next(fileobj).strip() | |
return replacements, _atom.findall(molecule) | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as f: | |
replacements, molecule = read_input(f) | |
count = len(set(one_step_substitute(molecule, replacements))) | |
print('Part 1:', count) | |
steps = random_replace(molecule, replacements) | |
print('Part 2:', steps) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from itertools import product, combinations | |
from collections import namedtuple | |
import re | |
Item = namedtuple('Item', 'name cost damage armour') | |
conv = lambda n: int(n) if n.isdigit() else n | |
load = lambda lines: [Item(*map(conv, re.split(r'\s{2,}', l))) for l in lines] | |
weapons = load('''\ | |
Dagger 8 4 0 | |
Shortsword 10 5 0 | |
Warhammer 25 6 0 | |
Longsword 40 7 0 | |
Greataxe 74 8 0 | |
'''.splitlines()) | |
# Armour is optional, include a noop. | |
armour = load('''\ | |
Leather 13 0 1 | |
Chainmail 31 0 2 | |
Splintmail 53 0 3 | |
Bandedmail 75 0 4 | |
Platemail 102 0 5 | |
None 0 0 0 | |
'''.splitlines()) | |
# Rings are optional; we pick between 0 and 2 so include 2 noops. | |
rings = load('''\ | |
Damage +1 25 1 0 | |
Damage +2 50 2 0 | |
Damage +3 100 3 0 | |
Defense +1 20 0 1 | |
Defense +2 40 0 2 | |
Defense +3 80 0 3 | |
None 0 0 0 | |
None 0 0 0 | |
'''.splitlines()) | |
def attack(weapon, rings, boss_armour): | |
return max(1, weapon.damage + sum(r.damage for r in rings) - boss_armour) | |
def defence(armour, rings, boss_attack): | |
return max(1, boss_attack - armour.armour - sum(r.armour for r in rings)) | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as f: | |
boss_hp = int(next(f).rpartition(':')[-1]) | |
boss_attack = int(next(f).rpartition(':')[-1]) | |
boss_armour = int(next(f).rpartition(':')[-1]) | |
player_hp = 100 | |
min_cost = min( | |
weapon.cost + armour.cost + sum(r.cost for r in rings) | |
for weapon, armour, rings in product( | |
weapons, armour, combinations(rings, 2)) | |
if (boss_hp // attack(weapon, rings, boss_armour) <= | |
player_hp // defence(armour, rings, boss_attack))) | |
print('Part 1:', min_cost) | |
max_cost = max( | |
weapon.cost + armour.cost + sum(r.cost for r in rings) | |
for weapon, armour, rings in product( | |
weapons, armour, combinations(rings, 2)) | |
if (boss_hp // attack(weapon, rings, boss_armour) > | |
player_hp // defence(armour, rings, boss_attack))) | |
print('Part 2:', max_cost) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import namedtuple | |
from functools import reduce | |
from heapq import heappop, heappush | |
from itertools import count | |
class Spell(namedtuple('BaseSpell', | |
'name cost effect turns damage heal armour mana')): | |
def __new__(cls, name, cost, effect=False, turns=None, damage=0, heal=0, | |
armour=0, mana=0): | |
return super().__new__( | |
cls, name, cost, effect, turns, damage, heal, armour, mana) | |
spells = ( | |
Spell('Magic Missile', 53, damage=4), | |
Spell('Drain', 73, damage=2, heal=2), | |
Spell('Shield', 113, effect=True, turns=6, armour=7), | |
Spell('Poison', 173, effect=True, turns=6, damage=3), | |
Spell('Recharge', 229, effect=True, turns=5, mana=101), | |
) | |
class State(object): | |
def __init__(self, hp, mana, boss_hp, boss_damage, | |
mana_spent=0, effects=None, hard=False, | |
parent=None, spell_cast=None): | |
self.hp = hp | |
self.mana = mana | |
self.boss_hp = boss_hp | |
self.boss_damage = boss_damage | |
self.mana_spent = mana_spent | |
self.effects = effects or () | |
self.hard = hard | |
self._parent = parent | |
self._spell_cast = spell_cast | |
def __eq__(self, other): | |
if not isinstance(other, State): | |
return NotImplemented | |
return all(getattr(self, k) == getattr(other, k) | |
for k in vars(self) if k[0] != '_') | |
def __hash__(self): | |
return reduce(lambda a, b: a ^ hash(b), | |
(v for k, v in vars(self).items() if k[0] != '_'), 0) | |
def iter_path(self): | |
if self._parent is None: | |
return | |
yield from self._parent.iter_path() | |
yield self._spell_cast | |
def process_effects(self, effects, hp, mana, boss_hp): | |
remaining_effects = [] | |
armour = 0 # either Shield is in effect or it is not | |
for timer, effect in self.effects: | |
hp += effect.heal | |
mana += effect.mana | |
boss_hp -= effect.damage | |
armour = max(armour, effect.armour) | |
if timer > 1: | |
remaining_effects.append((timer - 1, effect)) | |
return tuple(remaining_effects), hp, mana, boss_hp, armour | |
def boss_turn(self): | |
self.effects, self.hp, self.mana, self.boss_hp, armour = ( | |
self.process_effects( | |
self.effects, self.hp, self.mana, self.boss_hp)) | |
# only if the boss is still alive can they attack! | |
if self.boss_hp > 0: | |
self.hp -= max(1, self.boss_damage - armour) | |
def transitions(self): | |
# Player turn first | |
effects, hp, mana, boss_hp, __ = self.process_effects( | |
self.effects, self.hp - int(self.hard), self.mana, self.boss_hp) | |
for spell in spells: | |
if spell.cost > mana or any(spell is s for t, s in effects): | |
# can't cast spells for which we have no mana or in effect | |
continue | |
new_state = State( | |
hp, mana - spell.cost, boss_hp, self.boss_damage, | |
self.mana_spent + spell.cost, effects, hard=self.hard, | |
parent=self, spell_cast=spell.name) | |
if not spell.effect: | |
new_state.hp += spell.heal | |
new_state.boss_hp -= spell.damage | |
else: | |
new_state.effects = new_state.effects + ((spell.turns, spell),) | |
# Boss turn next | |
new_state.boss_turn() | |
# No point in playing a turn that has the player losing | |
if new_state.hp > 0: | |
yield new_state | |
def search_a_star(start): | |
open_states = {start} | |
pqueue = [(0, start)] | |
closed_states = set() | |
unique = count() | |
while open_states: | |
current = heappop(pqueue)[-1] | |
if current.boss_hp < 1: | |
return current | |
open_states.remove(current) | |
closed_states.add(current) | |
for state in current.transitions(): | |
if state in closed_states or state in open_states: | |
continue | |
open_states.add(state) | |
heappush(pqueue, (state.mana_spent, next(unique), state)) | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as f: | |
boss_hp = int(next(f).rpartition(':')[-1]) | |
boss_attack = int(next(f).rpartition(':')[-1]) | |
player_hp, player_mana = 50, 500 | |
start = State(player_hp, player_mana, boss_hp, boss_attack) | |
end = search_a_star(start) | |
print('Part 1:', end.mana_spent) | |
if '-v' in sys.argv: | |
print(*end.iter_path(), sep=' -> ') | |
start.hard = True | |
end = search_a_star(start) | |
print('Part 2:', end.mana_spent) | |
if '-v' in sys.argv: | |
print(*end.iter_path(), sep=' -> ') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
class CPU(object): | |
def __init__(self, program, register_a=0): | |
self.registers = {'a': register_a, 'b': 0} | |
self.pos = 0 | |
self.program = program | |
def run(self): | |
while 0 <= self.pos < len(self.program): | |
self.pos = self.execute() | |
return self.registers['b'] | |
def execute(self ): | |
op, *args = self.program[self.pos] | |
return getattr(self, 'op_' + op)(*args) | |
def op_hlf(self, r): | |
self.registers[r] >>= 1 | |
return self.pos + 1 | |
def op_tpl(self, r): | |
self.registers[r] *= 3 | |
return self.pos + 1 | |
def op_inc(self, r): | |
self.registers[r] += 1 | |
return self.pos + 1 | |
def op_jmp(self, offset): | |
return self.pos + int(offset) | |
def op_jie(self, r, offset): | |
return self.pos + (int(offset) if not self.registers[r] % 2 else 1) | |
def op_jio(self, r, offset): | |
return self.pos + (int(offset) if self.registers[r] == 1 else 1) | |
if __name__ == '__main__': | |
import sys | |
filename = sys.argv[-1] | |
with open(filename) as f: | |
instructions = [re.split(r'[ ,]+', line.strip()) for line in f] | |
cpu = CPU(instructions) | |
register_b = cpu.run() | |
print('Part 1:', register_b) | |
cpu = CPU(instructions, 1) | |
register_b = cpu.run() | |
print('Part 2:', register_b) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment