Skip to content

Commit

Permalink
Day 15 (#22)
Browse files Browse the repository at this point in the history
* Solve 15.a

* Solve 15.b

* Minor refactor

* More refactor

* Fix unbound var
  • Loading branch information
tyler-hoffman authored Dec 15, 2024
1 parent 0d9af04 commit f2afb25
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 0 deletions.
Empty file added aoc_2024/day_15/__init__.py
Empty file.
90 changes: 90 additions & 0 deletions aoc_2024/day_15/a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from dataclasses import dataclass
from functools import cached_property
from aoc_2024.day_15.parser import Parser
from aoc_2024.utils.point import Point

UP = Point(0, -1)
DOWN = Point(0, 1)
LEFT = Point(-1, 0)
RIGHT = Point(1, 0)


@dataclass
class Day15PartASolver:
grid: list[list[str]]
instructions: list[str]

@property
def solution(self) -> int:
# move
p = self.get_robot()
for direction in self.directions:
moved = self.shift(p, direction)
if moved:
p = p.add(direction)
assert self.points[p] == "@"
# get points:
boxes = {p for p, ch in self.points.items() if ch == "O"}
return sum([self.gps_score(box) for box in boxes])

def shift(self, p: Point, direction: Point) -> bool:
if self.points[p] == "#":
return False
elif self.points[p] == ".":
return True
else:
next = p.add(direction)
move = self.shift(p=next, direction=direction)
if move:
self.points[next] = self.points[p]
self.points[p] = "."
return move

@cached_property
def directions(self) -> list[Point]:
return [self.instruction_to_direction(inst) for inst in self.instructions]

def instruction_to_direction(self, instruction: str) -> Point:
return {
"<": LEFT,
">": RIGHT,
"^": UP,
"v": DOWN,
}[instruction]

def gps_score(self, p: Point) -> int:
return p.x + 100 * p.y

def get_robot(self) -> Point:
for point, value in self.points.items():
if value == "@":
return point
assert False, "better not get here lol"

@cached_property
def points(self) -> dict[Point, str]:
output: dict[Point, str] = {}
for y, line in enumerate(self.grid):
for x, ch in enumerate(line):
output[Point(x, y)] = ch
return output


def solve(input: str) -> int:
grid, instructions = Parser.parse(input)
solver = Day15PartASolver(
grid=grid,
instructions=instructions,
)

return solver.solution


def get_solution() -> int:
with open("aoc_2024/day_15/input.txt", "r") as f:
input = f.read()
return solve(input)


if __name__ == "__main__":
print(get_solution())
142 changes: 142 additions & 0 deletions aoc_2024/day_15/b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from dataclasses import dataclass
from functools import cached_property
from aoc_2024.day_15.parser import Parser
from aoc_2024.utils.point import Point

UP = Point(0, -1)
DOWN = Point(0, 1)
LEFT = Point(-1, 0)
RIGHT = Point(1, 0)


@dataclass
class Day15PartBSolver:
grid: list[list[str]]
instructions: list[str]

@property
def solution(self) -> int:
# move
p = self.get_robot()
for direction in self.directions:
can_shift = self.can_shift(p, direction)
if can_shift:
self.shift(p, direction)
p = p.add(direction)
assert self.points[p] == "@"

# get points:
boxes = {p for p, ch in self.points.items() if ch == "["}
return sum([self.gps_score(box) for box in boxes])

def can_shift(self, p: Point, direction: Point) -> bool:
char = self.points[p]
if char == "#":
return False
elif char == ".":
return True
elif direction in {LEFT, RIGHT} or char == "@":
next = p.add(direction)
return self.can_shift(p=next, direction=direction)
else:
assert direction in {UP, DOWN}
next = p.add(direction)
if self.points[p] == "[":
move_left = self.can_shift(p=next, direction=direction)
move_right = self.can_shift(p=next.add(RIGHT), direction=direction)
return move_left and move_right
elif self.points[p] == "]":
move_left = self.can_shift(p=next.add(LEFT), direction=direction)
move_right = self.can_shift(p=next, direction=direction)
return move_left and move_right
else:
assert False, "invalid?"

def shift(self, p: Point, direction: Point) -> None:
char = self.points[p]
if char in "[@]":
if direction in {LEFT, RIGHT} or char == "@":
next = p.add(direction)
self.shift(next, direction)
self.shift_cell(p, direction)
elif char in "[]":
if char == "[":
left = p
right = p.add(RIGHT)
else:
left = p.add(LEFT)
right = p

self.shift(left.add(direction), direction)
self.shift(right.add(direction), direction)

self.shift_cell(left, direction)
self.shift_cell(right, direction)

def shift_cell(self, p: Point, direction: Point) -> None:
next = p.add(direction)
self.points[next] = self.points[p]
self.points[p] = "."

@cached_property
def directions(self) -> list[Point]:
return [self.instruction_to_direction(inst) for inst in self.instructions]

def instruction_to_direction(self, instruction: str) -> Point:
return {
"<": LEFT,
">": RIGHT,
"^": UP,
"v": DOWN,
}[instruction]

def gps_score(self, p: Point) -> int:
return p.x + 100 * p.y

def get_robot(self) -> Point:
for point, value in self.points.items():
if value == "@":
return point
assert False, "better not get here lol"

@cached_property
def points(self) -> dict[Point, str]:
output: dict[Point, str] = {}
for y, line in enumerate(self.grid):
for x, ch in enumerate(line):
left = Point(x * 2, y)
right = left.add(RIGHT)
match ch:
case "#":
output[left] = "#"
output[right] = "#"
case ".":
output[left] = "."
output[right] = "."
case "O":
output[left] = "["
output[right] = "]"
case "@":
output[left] = "@"
output[right] = "."
return output


def solve(input: str) -> int:
grid, instructions = Parser.parse(input)
solver = Day15PartBSolver(
grid=grid,
instructions=instructions,
)

return solver.solution


def get_solution() -> int:
with open("aoc_2024/day_15/input.txt", "r") as f:
input = f.read()
return solve(input)


if __name__ == "__main__":
print(get_solution())
Binary file added aoc_2024/day_15/from_prompt.py
Binary file not shown.
Binary file added aoc_2024/day_15/input.txt
Binary file not shown.
18 changes: 18 additions & 0 deletions aoc_2024/day_15/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Parser:
@staticmethod
def parse(input: str) -> tuple[list[list[str]], list[str]]:
lines = input.strip().splitlines()
space_index = lines.index("")

return Parser.parse_grid(lines[:space_index]), Parser.parse_instructions(
lines[space_index + 1 :]
)

@staticmethod
def parse_grid(lines: list[str]) -> list[list[str]]:
return [list(line) for line in lines]

@staticmethod
def parse_instructions(lines: list[str]) -> list[str]:
combined = "".join(lines)
return list(combined)
Empty file added tests/test_day_15/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/test_day_15/test_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest
from aoc_2024.day_15.a import get_solution, solve
from aoc_2024.day_15.from_prompt import (
SAMPLE_DATA_SMALL,
SAMPLE_DATA_LARGE,
SAMPLE_SOLUTION_A_LARGE,
SAMPLE_SOLUTION_A_SMALL,
SOLUTION_A,
)


@pytest.mark.parametrize(
("input", "expected"),
[
(SAMPLE_DATA_LARGE, SAMPLE_SOLUTION_A_LARGE),
(SAMPLE_DATA_SMALL, SAMPLE_SOLUTION_A_SMALL),
],
)
def test_solve(input: str, expected: int):
assert solve(input) == expected


def test_my_solution():
assert get_solution() == SOLUTION_A
14 changes: 14 additions & 0 deletions tests/test_day_15/test_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from aoc_2024.day_15.b import get_solution, solve
from aoc_2024.day_15.from_prompt import (
SAMPLE_DATA_LARGE,
SAMPLE_SOLUTION_B_LARGE,
SOLUTION_B,
)


def test_solve():
assert solve(SAMPLE_DATA_LARGE) == SAMPLE_SOLUTION_B_LARGE


def test_my_solution():
assert get_solution() == SOLUTION_B

0 comments on commit f2afb25

Please sign in to comment.