Lines 1 to 12 in 86d3174
from pathlib import Path | |
INPUT_FILE = "input1.txt" | |
elf_foods = [ | |
sum([int(line) for line in section.splitlines() if line]) | |
for section in Path(INPUT_FILE).read_text().split("\n\n") | |
] | |
part1 = max(elf_foods) | |
part2 = sum(sorted(elf_foods, reverse=True)[:3]) | |
print(f"Part1: {part1}") | |
print(f"Part2: {part2}") |
Lines 1 to 29 in 86d3174
from pathlib import Path | |
SCORES = { | |
"A": {"Y": 6, "X": 3, "Z": 0}, | |
"B": {"Y": 3, "X": 0, "Z": 6}, | |
"C": {"Y": 0, "X": 6, "Z": 3}, | |
} | |
def points(c): | |
return ord(c) - ord("X") + 1 | |
def score(opponent, me): | |
return SCORES[opponent][me] + points(me) | |
SCORES2 = { | |
"A": {"Y": 1 + 3, "X": 3 + 0, "Z": 2 + 6}, | |
"B": {"Y": 2 + 3, "X": 1 + 0, "Z": 3 + 6}, | |
"C": {"Y": 3 + 3, "X": 2 + 0, "Z": 1 + 6}, | |
} | |
input_pairs = [line.split(" ") for line in Path("input2.txt").read_text().splitlines()] | |
part1 = sum([score(opponent, me) for (opponent, me) in input_pairs]) | |
part2 = sum([SCORES2[opponent][strategy] for (opponent, strategy) in input_pairs]) | |
print(f"Part1: {part1}") | |
print(f"Part2: {part2}") |
Lines 1 to 34 in 86d3174
from pathlib import Path | |
def priority(c): | |
if c.isupper(): | |
return ord(c) - ord("A") + 27 | |
return ord(c) - ord("a") + 1 | |
part1 = sum( | |
[ | |
priority(*(set(line[:s]) & set(line[s:]))) # should be exactile one | |
for line in Path("input3.txt").read_text().splitlines() | |
if (s := len(line) // 2) | |
] | |
) | |
print(f"Part1: {part1}") | |
lines = [ | |
line | |
for line in Path("input3.txt").read_text().splitlines() | |
if (s := len(line) // 2) | |
] | |
part2 = sum( | |
[ | |
priority(*set.intersection(*[set(sack) for sack in lines[i : (i + 3)]])) | |
for i in range(0, len(lines), 3) | |
] | |
) | |
print(f"Part2: {part2}") |
Lines 1 to 32 in 86d3174
import re | |
from pathlib import Path | |
def parse_line(line): | |
f1, t1, f2, t2 = [ | |
int(s) for s in re.match("^([0-9]+)-([0-9]+),([0-9]+)-([0-9]+)$", line).groups() | |
] | |
return set(range(f1, t1 + 1)), set(range(f2, t2 + 1)) | |
def is_included(s1, s2): | |
return s1 & s2 in (s1, s2) | |
part1 = sum( | |
[ | |
is_included(*parse_line(line)) | |
for line in Path("input4.txt").read_text().splitlines() | |
] | |
) | |
part2 = sum( | |
[ | |
len(set.intersection(*parse_line(line))) >= 1 | |
for line in Path("input4.txt").read_text().splitlines() | |
] | |
) | |
print(f"Part1: {part1}") | |
print(f"Part2: {part2}") |
Lines 1 to 49 in 86d3174
from pathlib import Path | |
from dataclasses import dataclass | |
import re | |
@dataclass | |
class State: | |
stacks: list[list[str]] | |
def move(self, n: int, src: int, dst: int, reverse=True): | |
items = self.stacks[src - 1][-n:] | |
if reverse: | |
items = reversed(items) | |
self.stacks[src - 1] = self.stacks[src - 1][:-n] | |
self.stacks[dst - 1] = [*self.stacks[dst - 1], *items] | |
def __str__(self): | |
"Top representation of stacks" | |
return "".join([stack[-1] for stack in self.stacks if len(stack)]) | |
def parse_input() -> (State, tuple[int, int, int]): | |
sections = Path("input5.txt").read_text().split("\n\n") | |
state_lines = sections[0].splitlines() | |
crate_positions = [ | |
match.span()[0] for match in (re.finditer("[0-9]+", state_lines[-1])) | |
] | |
stacks: list[str] = [[] for _ in range(len(crate_positions))] | |
for crate_str in state_lines[:-1]: | |
for offset, i in zip(crate_positions, range(len(crate_positions))): | |
if len(crate_str) > offset and (c := crate_str[offset]).isalpha(): | |
stacks[i] = [c, *stacks[i]] | |
commands = [ | |
[ | |
int(s) | |
for s in re.match("move ([0-9]+) from ([0-9]+) to ([0-9+])", line).groups() | |
] | |
for line in sections[1].splitlines() | |
] | |
return State(stacks), commands | |
state, movements = parse_input() | |
state2 = State([*state.stacks]) | |
for movement in movements: | |
state.move(*movement) | |
state2.move(*movement, False) | |
print(f"Part1: {state}") | |
print(f"Part2: {state2}") |
Lines 1 to 7 in 86d3174
from pathlib import Path | |
line = Path("input6.txt").read_text().splitlines()[0] | |
for p, n in ((1, 4), (2, 14)): | |
offset = [len(set(line[i : i + n])) for i in range(len(line) - n)].index(n) | |
print(f"Part{p}: {offset+n}") |
Lines 1 to 64 in 86d3174
from pathlib import Path | |
import re | |
SIZE_RE = re.compile("([0-9]+) (.*)$") | |
DIR_RE = re.compile("dir (.*)$") | |
COMMAND_RE = re.compile(r"(ls|cd )(.*)$") | |
size = 0 | |
p = Path("/") | |
sizes = {} | |
current_command = None | |
s = Path("input7.txt").read_text() | |
cmd_outputs = re.split(r"\$ ", s, flags=re.MULTILINE)[1:] | |
dir_tree = {Path("/"): []} | |
cur_path = Path("/") | |
for cmd_output in cmd_outputs: | |
cmd_s = cmd_output.splitlines()[0] | |
output_lines = cmd_output.splitlines()[1:] | |
m = COMMAND_RE.match(cmd_s) | |
cmd, arg = m.groups() | |
if cmd == "ls": | |
if len(dir_tree.get(cur_path, [])): | |
raise ValueError(f"{cur_path} entries already exist") | |
dir_tree[cur_path] = [] | |
for output in output_lines: | |
if m := SIZE_RE.match(output): | |
size, name = int(m.group(1)), m.group(2) | |
dir_tree[cur_path].append((size, name)) | |
elif m := DIR_RE.match(output): | |
name = m.group(1) | |
dir_tree[cur_path].append((0, name)) | |
else: | |
raise ValueError("Invalid ls line {output}") | |
elif cmd == "cd ": | |
cur_path = (cur_path / arg).resolve() | |
dir_tree[cur_path] = dir_tree.get(cur_path, []) | |
else: | |
raise ValueError(f"Invalid Command '{cmd}'") | |
def dir_size(p): | |
acc = 0 | |
for size, name in dir_tree.get(p): | |
if size: | |
acc += size | |
else: | |
acc += dir_size(p / Path(name)) | |
return acc | |
sum1 = sum([x for p in dir_tree.keys() if (x := dir_size(p)) <= 100_000]) | |
print(f"Part1: {sum1}") | |
DISK_SIZE = 70000000 | |
used_space = dir_size(Path("/")) | |
free_space = DISK_SIZE - used_space | |
to_delete = 30000000 - free_space | |
min2 = min([x for p in dir_tree.keys() if (x := dir_size(p)) >= to_delete]) | |
print(f"Part2: {min2}") |
Lines 1 to 55 in 86d3174
from pathlib import Path | |
grid = [[int(c) for c in line] for line in Path("input8.txt").read_text().splitlines()] | |
def visible_nodes(grid): | |
nodes = [] | |
for y in range(1, len(grid) - 1): | |
for x in range(1, len(grid[y]) - 1): | |
height = grid[y][x] | |
inv_y_t = any((grid[y1][x] >= height for y1 in range(0, y))) | |
inv_y_b = any((grid[y1][x] >= height for y1 in range(y + 1, len(grid)))) | |
inv_x_l = any((grid[y][x1] >= height for x1 in range(0, x))) | |
inv_x_r = any((grid[y][x1] >= height for x1 in range(x + 1, len(grid[y])))) | |
visible = not all( | |
[ | |
inv_y_b, | |
inv_y_t, | |
inv_x_r, | |
inv_x_l, | |
] | |
) | |
if visible: | |
nodes.append((y, x, height, visible)) | |
return nodes | |
def sight(grid, y, x): | |
height = grid[y][x] | |
ranges = [ | |
zip([y] * len(grid), range(x - 1, -1, -1)), | |
zip([y] * len(grid), range(x + 1, len(grid))), | |
zip(range(y - 1, -1, -1), [x] * len(grid)), | |
zip(range(y + 1, len(grid)), [x] * len(grid)), | |
] | |
total = 1 | |
for r in ranges: | |
acc = 0 | |
for y, x in r: | |
acc += 1 | |
if grid[y][x] >= height: | |
break | |
total *= acc | |
return total | |
def score_nodes(grid): | |
for y in range(1, len(grid) - 1): | |
for x in range(1, len(grid[y]) - 1): | |
yield sight(grid, x, y) | |
n = len(visible_nodes(grid)) + len(grid) * 4 - 4 | |
print(f"Part1: {n}") | |
print(f"Part2: {max(score_nodes(grid))}") |
Lines 1 to 68 in 86d3174
from pathlib import Path | |
movements = [ | |
(cols[0], int(cols[1])) | |
for line in Path("input9.txt").read_text().splitlines() | |
if (cols := line.split()) | |
] | |
t_x = 0 | |
t_y = 0 | |
h_x = 0 | |
h_y = 0 | |
visited_positions = set((t_x, t_y)) | |
for (direction, n) in movements: | |
for _ in range(n): | |
match direction: | |
case "R": | |
h_x += 1 | |
case "U": | |
h_y -= 1 | |
case "D": | |
h_y += 1 | |
case "L": | |
h_x -= 1 | |
case _: | |
raise ValueError("Invalid move: {direction}") | |
dist_x = h_x - t_x | |
dist_y = h_y - t_y | |
match (dist_x, dist_y): | |
case (2, 0): | |
t_x += 1 | |
visited_positions.add((t_x, t_y)) | |
case (-2, 0): | |
t_x -= 1 | |
visited_positions.add((t_x, t_y)) | |
case (2, 1) | (1, 2): | |
t_x += 1 | |
t_y += 1 | |
visited_positions.add((t_x, t_y)) | |
case (2, -1) | (1, -2): | |
t_x += 1 | |
t_y -= 1 | |
visited_positions.add((t_x, t_y)) | |
case (0, 2): | |
t_y += 1 | |
visited_positions.add((t_x, t_y)) | |
case (0, -2): | |
t_y -= 1 | |
visited_positions.add((t_x, t_y)) | |
case (-1, 2) | (-2, 1): | |
t_x -= 1 | |
t_y += 1 | |
visited_positions.add((t_x, t_y)) | |
case (-2, -1) | (-1, -2): | |
t_x -= 1 | |
t_y -= 1 | |
visited_positions.add((t_x, t_y)) | |
case (1, 1) | (-1, -1) | (0, 1) | (1, 0) | (0, -1) | (-1, 0) | (1, -1) | ( | |
0, | |
0, | |
) | ( | |
-1, | |
1, | |
): | |
pass | |
case _: | |
raise ValueError(f"Invalid move {direction} {dist_x, dist_y}") | |
print(len(visited_positions)) |
Lines 1 to 34 in 86d3174
from pathlib import Path | |
lines = Path("input10.txt").read_text().splitlines() | |
cycle = 1 | |
r = 1 | |
strengths = {} | |
positions = {} | |
for line in lines: | |
cmd = line.split() | |
strengths[cycle] = r * cycle | |
positions[cycle] = r | |
if cmd[0] == "addx": | |
n = int(cmd[1]) | |
strengths[cycle + 1] = r * (cycle + 1) | |
positions[cycle + 1] = r | |
r += n | |
strengths[cycle + 2] = r * (cycle + 2) | |
positions[cycle + 2] = r | |
cycle += 2 | |
elif cmd[0] == "noop": | |
cycle += 1 | |
offsets = [20, 60, 100, 140, 180, 220] | |
print(f"Part1: {sum([strengths[offset] for offset in offsets])}") | |
print("Part2:") | |
for i in range(1, 241): | |
if i % 40 == 1: | |
print() | |
pos = positions[i] | |
if i % 40 in (pos + 1, pos + 2, pos): | |
print("#", end="") | |
else: | |
print(".", end="") |
Lines 1 to 96 in 86d3174
import re | |
from dataclasses import dataclass | |
from functools import reduce | |
from operator import add, mul | |
from pathlib import Path | |
@dataclass | |
class Monkey: | |
n: int | |
items: list[int] | |
operation: str | |
x: str | |
mod: int | |
monkey1: int | |
monkey2: int | |
inspections: int = 0 | |
def inspect(self, monkeys: list[Monkey], mod=None): | |
for item in self.items: | |
self.inspections += 1 | |
if self.operation == "*": | |
op = mul | |
else: | |
op = add | |
if self.x == "old": | |
item = op(item, item) | |
else: | |
item = op(item, int(self.x)) | |
if mod: | |
item = item % mod | |
else: | |
item = item // 3 | |
if item % self.mod == 0: | |
monkeys[self.monkey1].items.append(item) | |
else: | |
monkeys[self.monkey2].items.append(item) | |
self.items = [] | |
monkey_strs = [ | |
part.strip() | |
for part in re.split(r"^$", Path("input11.txt").read_text(), flags=re.MULTILINE) | |
] | |
def parse_monkey(s: str): | |
(n, items_str, op, x, mod, monkey1, monkey2) = re.match( | |
"""Monkey ([0-9]): | |
*Starting items: ([0-9, ]+) | |
*Operation: new = old (.) (.+) | |
*Test: divisible by ([0-9]+) | |
*If true: throw to monkey ([0-9]+) | |
*If false: throw to monkey ([0-9]+)""", | |
s.strip(), | |
flags=re.MULTILINE, | |
).groups() | |
return Monkey( | |
int(n), | |
[int(s) for s in items_str.split(", ")], | |
op, | |
x, | |
int(mod), | |
int(monkey1), | |
int(monkey2), | |
) | |
def parse_monkeys(monkey_strs): | |
return [ | |
parse_monkey(monkey_str) | |
for monkey_str in monkey_strs | |
if monkey_str.startswith("Monkey") | |
] | |
monkeys = parse_monkeys(monkey_strs) | |
def interact(times: int, mod=None): | |
for i in range(times): | |
for monkey in monkeys: | |
monkey.inspect(monkeys, mod) | |
interact(20) | |
i1, i2 = sorted(monkey.inspections for monkey in monkeys)[-2:] | |
print(f"Part1: {i1*i2}") | |
monkeys = parse_monkeys(monkey_strs) | |
mod = reduce(mul, [monkey.mod for monkey in monkeys]) | |
interact(10000, mod) | |
i1, i2 = sorted(monkey.inspections for monkey in monkeys)[-2:] | |
print(f"Part2: {i1*i2}") |
Lines 1 to 89 in 86d3174
from pathlib import Path | |
from typing import Iterator | |
class Grid: | |
lines: list[list[str]] | |
pos: tuple[int, int] | |
target: tuple[int, int] | |
best: int = 10000 | |
def __init__(self, file_name: str): | |
self.lines = [ | |
list(iter(line)) for line in Path(file_name).read_text().splitlines() | |
] | |
for y, line in enumerate(self.lines): | |
for x, s in enumerate(line): | |
if s == "S": | |
self.lines[y][x] = "a" | |
self.pos = (y, x) | |
elif s == "E": | |
self.lines[y][x] = "z" | |
self.target = (y, x) | |
def __str__(self): | |
return f"""player: {self.pos} | |
target: {self.target} | |
""" + "\n".join( | |
["".join(line) for line in self.lines] | |
) | |
def elevation(self, src: tuple[int, int], dst: tuple[int, int]): | |
if ( | |
dst[0] < 0 | |
or dst[0] >= len(self.lines) | |
or dst[1] >= len(self.lines[dst[0]]) | |
or dst[1] < 0 | |
): | |
return 1000 | |
diff = ord(self.lines[dst[0]][dst[1]]) - ord(self.lines[src[0]][src[1]]) | |
return diff | |
def posible_movements(self, pos: tuple[int, int]) -> Iterator[tuple[int, int]]: | |
for ydiff in (-1, 1): | |
if self.elevation(pos, (pos[0] + ydiff, pos[1])) <= 1: | |
yield (pos[0] + ydiff, pos[1]) | |
for xdiff in (-1, 1): | |
if self.elevation(pos, (pos[0], pos[1] + xdiff)) <= 1: | |
yield (pos[0], pos[1] + xdiff) | |
def traverse(self, start_pos=None): | |
if start_pos: | |
self.pos = start_pos | |
distances = {self.pos: (0, None)} | |
while self.target not in distances: | |
prev_keys = list(distances.keys()) | |
progress = False | |
for q in prev_keys: | |
cost, prev = distances[q] | |
for new_pos in self.posible_movements(q): | |
if new_pos not in distances: | |
distances[new_pos] = cost + 1, q | |
progress = True | |
if not progress: | |
return | |
return distances | |
def starting_positions(self): | |
return [ | |
(y, x) | |
for y, row in enumerate(self.lines) | |
for x, c in enumerate(row) | |
if c == "a" | |
] | |
grid = Grid("input12.txt") | |
m = grid.traverse() | |
print(f"Part1: {m[grid.target][0]}") | |
min_2 = min( | |
[ | |
z[grid.target][0] | |
for start_pos in grid.starting_positions() | |
if (z := grid.traverse(start_pos)) | |
] | |
) | |
print(f"Part2: {min_2}") |
Lines 1 to 33 in 86d3174
from pathlib import Path | |
from functools import cmp_to_key | |
import ast | |
line_pairs = [lines.split() for lines in Path("input13.txt").read_text().split("\n\n")] | |
input_pairs = [(ast.literal_eval(a), ast.literal_eval(b)) for a, b in line_pairs] | |
first_pair = input_pairs[0] | |
def compare(a, b): | |
if type(a) is int and type(b) is int: | |
return a - b | |
if type(a) is int: | |
return compare([a], b) | |
if type(b) is int: | |
return compare(a, [b]) | |
for x, y in zip(a, b): | |
if r := compare(x, y): | |
return r | |
return len(a) - len(b) | |
s = sum((i + 1 for i, pair in enumerate(input_pairs) if compare(*pair) < 0)) | |
print(f"Part1: {s}") | |
DIVIDERS = [[[2]], [[6]]] | |
all_signals = [p for pair in input_pairs for p in pair] + DIVIDERS | |
sorted_signals = sorted(all_signals, key=cmp_to_key(compare)) | |
s = (sorted_signals.index(DIVIDERS[0]) + 1) * (sorted_signals.index(DIVIDERS[1]) + 1) | |
print(f"Part2: {s}") |
Lines 1 to 86 in 86d3174
from pathlib import Path | |
from dataclasses import dataclass | |
from functools import reduce | |
import re | |
SB_PATTERN = re.compile( | |
r"^Sensor at x=(-?[0-9]+), y=(-?[0-9]+): closest beacon is at x=(-?[0-9]+), y=(-?[0-9]+)$" | |
) | |
@dataclass(frozen=True) | |
class Sensor: | |
x: int | |
y: int | |
distance: int | |
@dataclass(frozen=True) | |
class Beacon: | |
x: int | |
y: int | |
beacons: set[Beacon] = set() | |
sensors: set[Sensor] = set() | |
def merge_ranges(ranges): | |
ret = [ranges[0]] | |
for low, up in ranges[1:]: | |
if low - 1 <= ret[-1][1]: | |
ret[-1] = ret[-1][0], max(ret[-1][1], up) | |
else: | |
ret.append((low, up)) | |
return ret | |
for line in Path("input15.txt").read_text().splitlines(): | |
sx, sy, bx, by = [int(x) for x in SB_PATTERN.match(line).groups()] | |
distance = abs(sx - bx) + abs(sy - by) | |
sensors.add(Sensor(sx, sy, distance)) | |
beacons.add(Beacon(bx, by)) | |
Y = 2000000 | |
def get_ranges(sensors, y=Y): | |
ranges = [] | |
for sensor in sensors: | |
x_len = sensor.distance - abs(sensor.y - y) | |
if x_len > 0: | |
if sensor.y == y: | |
ranges.append((sensor.x - x_len, sensor.x - 1)) | |
ranges.append((sensor.x + 1, sensor.x + x_len)) | |
else: | |
ranges.append((sensor.x - x_len, sensor.x + x_len)) | |
ranges.sort() | |
ranges = merge_ranges(ranges) | |
return ranges | |
ranges = get_ranges(sensors, Y) | |
free = reduce(lambda acc, r: acc + r[1] - r[0], ranges, 0) | |
print(f"Part1: {free}") | |
MAX_Y = 4000000 | |
# Brute Force :-( | |
def get_positions(sensors, max_y=MAX_Y): | |
for y in range(0, max_y): | |
ranges = get_ranges(sensors, y) | |
if len(ranges): | |
for r1, r2 in zip(ranges, ranges[1:]): | |
for x in range(r1[1] + 1, r2[0]): | |
if ( | |
x >= 0 | |
and x < max_y | |
and x not in (sensor.x for sensor in sensors if sensor.y == y) | |
and x not in (beacon.x for beacon in beacons if beacon.y == y) | |
): | |
yield (y, x) | |
(y, x) = next(get_positions(sensors, MAX_Y)) | |
frequency = y + x * 4000000 | |
print(f"Part2: {frequency}") |
TODO
Lines 1 to 137 in 86d3174
from pathlib import Path | |
from dataclasses import dataclass | |
from itertools import cycle | |
movements = Path("input17.txt").read_text().strip() | |
shapes = [ | |
["@@@@"], | |
[".@.", "@@@", ".@."], | |
["..@", "..@", "@@@"], | |
["@", "@", "@", "@"], | |
["@@", "@@"], | |
] | |
start_positions = [()] | |
def shape_len(shape: list[str]): | |
return max([s.rindex("@") for s in shape]) + 1 | |
@dataclass | |
class Sprite: | |
x: int | |
y: int | |
shape: list[str] | |
class Game: | |
board = [] | |
current_sprite: Sprite | None | |
def __init__(self): | |
self.board = [] | |
self.current_sprite = None | |
def freepos_y(self): | |
for i, row in enumerate(self.board): | |
if "#" in row or "-" in row: | |
return i - 1 | |
return len(self.board) - 1 | |
def place_item(self, shape: list[str]): | |
if not self.board or self.freepos_y() - len(shape) < 10: # Ensure enough space | |
self.board = [" "] * 7 + self.board # extend board | |
y = self.freepos_y() - 2 - len(shape) | |
x = 2 | |
self.current_sprite = Sprite(x, y, shape) | |
def push(self, s: str): | |
match s: | |
case "<": | |
if self.current_sprite.x > 0: | |
self.current_sprite.x -= 1 | |
if self.colides(): | |
self.current_sprite.x += 1 # revert | |
case ">": | |
if self.current_sprite.x + shape_len(self.current_sprite.shape) < 7: | |
self.current_sprite.x += 1 | |
if self.colides(): | |
self.current_sprite.x -= 1 # revert | |
case _: | |
raise ValueError(f"Invalid movement {s}") | |
def colides(self) -> bool: | |
s = self.current_sprite | |
for y in range(0, len(s.shape)): | |
if y + s.y >= len(self.board): | |
return True | |
for x in range(len(s.shape[y])): | |
if s.shape[y][x] == "@" and self.board[y + s.y][x + s.x] == "#": | |
return True | |
return False | |
def freeze(self): | |
s = self.current_sprite | |
for y in range(0, len(s.shape)): | |
self.board[y + s.y] = ( | |
self.board[y + s.y][: s.x] | |
+ "".join( | |
[ | |
"#" if s == "@" else b | |
for b, s in zip(self.board[y + s.y][s.x :], s.shape[y]) | |
] | |
) | |
+ self.board[y + s.y][s.x + len(s.shape[y]) :] | |
) | |
self.current_sprite = None | |
def fall(self) -> bool: | |
self.current_sprite.y += 1 | |
if self.colides(): | |
self.current_sprite.y -= 1 # revert | |
self.freeze() | |
return False | |
return True | |
def height(self) -> int: | |
return len(self.board) - self.freepos_y() - 1 | |
def __str__(self): | |
merged_rows = [] | |
for y, row in enumerate(self.board): | |
s = self.current_sprite | |
if s and y in range(s.y, s.y + len(s.shape)): | |
current_line = ( | |
self.board[y][: s.x] | |
+ "".join( | |
[ | |
b if s == "." else s | |
for (b, s) in zip(self.board[y][s.x :], s.shape[y - s.y]) | |
] | |
) | |
+ self.board[y][s.x + len(s.shape[y - s.y]) :] | |
) | |
else: | |
current_line = self.board[y] | |
merged_rows.append(current_line) | |
return "\n".join(merged_rows) | |
g = Game() | |
cycled_shapes = cycle(shapes) | |
i = 0 | |
for move in cycle(movements): | |
if not g.current_sprite: | |
shape = next(cycled_shapes) | |
if i == 2022: | |
break | |
i += 1 | |
g.place_item(shape) | |
g.push(move) | |
g.fall() | |
part1 = g.height() | |
print(f"Part1: {part1}") |
Lines 1 to 23 in 86d3174
from pathlib import Path | |
cubes = { | |
tuple([int(c) for c in line.split(",")]) | |
for line in Path("input18.txt").read_text().strip().splitlines() | |
} | |
total_sides = 0 | |
for cube in cubes: | |
x, y, z = cube | |
sides = 0 | |
for i1, i2, i3 in ( | |
(1, 0, 0), | |
(-1, 0, 0), | |
(0, 1, 0), | |
(0, -1, 0), | |
(0, 0, 1), | |
(0, 0, -1), | |
): | |
if (x + i1, y + i2, z + i3) not in cubes: | |
sides += 1 | |
total_sides += sides | |
print(total_sides) |
Lines 1 to 33 in 86d3174
from pathlib import Path | |
xs = [int(line) for line in Path("input20.txt").read_text().splitlines()] | |
def decrypt(cs: list[int]): | |
n = len(cs) | |
ret = [(c, False) for c in cs] | |
count = 0 | |
while count < n: | |
for i in range(n): | |
c, visited = ret[i] | |
if not visited: | |
if c == 0: | |
ret[i] = (c, True) | |
count += 1 | |
break | |
target = (i + c) % (n - 1) | |
ret.pop(i) | |
if target == 0: # wrap arround | |
ret.append((c, True)) | |
else: | |
ret.insert(target, (c, True)) | |
count += 1 | |
break | |
return [x for x, _ in ret] | |
plain = decrypt(xs) | |
i = plain.index(0) | |
coord_sum = sum([plain[(i + offset) % len(plain)] for offset in (1000, 2000, 3000)]) | |
print(f"Part1: {coord_sum}") |
Lines 1 to 45 in 86d3174
from pathlib import Path | |
from operator import add, mul, floordiv, sub | |
import time | |
lines = Path("input21.txt").read_text().splitlines() | |
monkeys = {} | |
for line in lines: | |
match line.split(): | |
case [nameq, name1, ops, name2]: | |
monkeys[nameq[:-1]] = (name1, ops, name2) | |
case [nameq, s]: | |
monkeys[nameq[:-1]] = int(s) | |
case _: | |
raise ValueError(f"Invalid line: {line}") | |
OPSMAP = {"+": add, "*": mul, "/": floordiv, "-": sub} | |
def yell(monkey, opsmap=OPSMAP): | |
match monkeys.get(monkey): | |
case (name1, op, name2): | |
return OPSMAP[op](yell(name1), yell(name2)) | |
case n: | |
return n | |
print(f"Part1: {yell('root')}") | |
# FIXME: Don't change constant | |
OPSMAP["="] = lambda x, y: (x, y) | |
monkeys["root"] = (monkeys["root"][0], "=", monkeys["root"][2]) | |
# binary search | |
low, high = 0, 2**64 | |
while True: | |
pivot = (low + high) // 2 | |
monkeys["humn"] = pivot | |
l, r = yell("root") | |
print(l, r, pivot, l - r) | |
if l == r: | |
break | |
if l < r: | |
high = pivot | |
else: | |
low = pivot | |
print(f"Part2: {pivot}") |
Lines 1 to 88 in 86d3174
from pathlib import Path | |
import re | |
COMMANDS_RE = re.compile("([0-9]+|[RL])") | |
DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0)] | |
class Game: | |
col: int | |
row: int | |
grid: list[str] | |
commands: list[str | int] | |
dir_index: int | |
def __init__(self, path: Path): | |
lines = path.read_text().splitlines() | |
self.grid = lines[:-2] | |
# ensure all lines have same length | |
m = max([len(line) for line in self.grid]) | |
self.grid = [line.ljust(m) for line in self.grid] | |
commands_s = lines[-1] | |
self.commands = [int(token) if token.isdigit() else token for token in COMMANDS_RE.findall(commands_s)] | |
self.col = self.start_col(0) | |
self.row = 0 | |
self.dir_index = 0 | |
def start_col(self, y): | |
return min(self.grid[y].index("."), self.grid[y].index("#")) | |
def end_col(self, y): | |
r_col = self.grid[y][::-1] | |
return len(r_col) - 1 - min(r_col.index("."), r_col.index("#")) | |
def start_row(self, x): | |
column = [row[x] for row in self.grid] | |
return min(column.index("."), column.index("#") if "#" in column else 1000) | |
def end_row(self, x): | |
r_column = [row[x] for row in self.grid][::-1] | |
return len(r_column) - 1 - min(r_column.index("."), r_column.index("#") if "#" in r_column else 1000) | |
def __str__(self): | |
return f""" | |
{self.col} | |
{self.row} | |
""" + "\n".join( | |
self.grid | |
) | |
def move(self) -> bool: | |
d = DIRECTIONS[self.dir_index] | |
if d[0]: # move row | |
new_row = self.row + d[0] | |
if new_row > self.end_row(self.col): | |
new_row = self.start_row(self.col) | |
elif new_row < self.start_row(self.col): | |
new_row = self.end_row(self.col) | |
if self.grid[new_row][self.col] == "#": | |
return False | |
self.row = new_row | |
return True | |
elif d[1]: | |
new_col = self.col + d[1] | |
if new_col > self.end_col(self.row): | |
new_col = self.start_col(self.row) | |
elif new_col < self.start_col(self.row): | |
new_col = self.end_col(self.row) | |
if self.grid[self.row][new_col] == "#": | |
return False | |
self.col = new_col | |
return True | |
def start(self) -> int: | |
for command in self.commands: | |
match command: | |
case int(command): | |
while command > 0 and self.move(): | |
command -= 1 | |
case "R": | |
self.dir_index = (self.dir_index + 1) % len(DIRECTIONS) # turn | |
case "L": | |
self.dir_index = (self.dir_index - 1) % len(DIRECTIONS) # turn | |
return (self.row + 1) * 1000 + 4 * (self.col + 1) + self.dir_index | |
game = Game(Path("input22.txt")) | |
print(f"Part1: {game.start()}") |
Lines 1 to 71 in 86d3174
from itertools import cycle, islice | |
from operator import attrgetter | |
from pathlib import Path | |
class Elf: | |
x: int | |
y: int | |
def __init__(self, y, x): | |
self.x = x | |
self.y = y | |
def __repr__(self): | |
return f"Elf({self.y, self.x})" | |
def position(self): | |
return (self.y, self.x) | |
class Game: | |
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1), (1, -1), (1, 0), (1, 1), (0, -1), (0, 1)] | |
def __init__(self, file="input.txt"): | |
lines = Path(file).read_text().splitlines() | |
self.elfs = [Elf(y, x) for y, line in enumerate(lines) for x, c in enumerate(line) if c == "#"] | |
self.looking_positions = cycle([(-1, -1), (-1, 0), (-1, 1), (1, -1), (1, 0), (1, 1), (-1, -1), (0, -1), (1, -1), (-1, 1), (0, 1), (1, 1)]) | |
def try_move(self, elf, check_positions, elf_positions) -> None | tuple[int, int]: | |
final_pos = (elf.y + check_positions[1][0], (elf.x + check_positions[1][1])) | |
is_occupied = any((elf.y + off_y, elf.x + off_x) in elf_positions for off_y, off_x in check_positions) | |
if not is_occupied: | |
return final_pos | |
def is_elf_around(self, elf, elf_positions): | |
return any([(elf.y + off_y, elf.x + off_x) in elf_positions for off_y, off_x in Game.DIRECTIONS]) | |
def rect_size(self): | |
y_min = min(self.elfs, key=attrgetter("y")).y | |
y_max = max(self.elfs, key=attrgetter("y")).y | |
x_min = min(self.elfs, key=attrgetter("x")).x | |
x_max = max(self.elfs, key=attrgetter("x")).x | |
return (y_max - y_min + 1) * (x_max - x_min + 1) - len(self.elfs) | |
def move(self) -> int: | |
elf_positions = {elf.position() for elf in self.elfs} | |
moves = {} | |
for _ in range(4): # all directions | |
check_positions = list(islice(self.looking_positions, 3)) | |
for elf in self.elfs: | |
if (self.is_elf_around(elf, elf_positions)) and (elf not in moves) and (new_pos := self.try_move(elf, check_positions, elf_positions)): | |
moves[elf] = new_pos | |
new_positions = list(moves.values()) | |
for elf, (new_y, new_x) in moves.items(): | |
if new_positions.count((new_y, new_x)) == 1: # not crowded | |
elf.y = new_y | |
elf.x = new_x | |
list(islice(self.looking_positions, 3)) # start in next direction next round | |
return len(new_positions) | |
if __name__ == "__main__": | |
g = Game("input23.txt") | |
for _ in range(10): | |
g.move() | |
print(f"Part1: {g.rect_size()}") | |
g = Game("input23.txt") | |
count = 1 | |
while g.move(): | |
count += 1 | |
print(f"Part2: {count}") |