Implement day 14 solver

This commit is contained in:
2024-12-13 22:18:56 -08:00
parent 4cbd1c9aee
commit 5ee410d26d
3 changed files with 135 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import re
from textwrap import dedent
from typing import Iterator, NamedTuple, override
from unittest import TestCase
from puzzles._solver import Solver

133
puzzles/14.py Normal file
View File

@@ -0,0 +1,133 @@
import re
from collections import Counter
from itertools import count
from math import prod
from textwrap import dedent
from typing import NamedTuple, override
from unittest import TestCase
from more_itertools import filter_map, first_true, iterate
from puzzles._solver import Solver
class Robot(NamedTuple):
x: int
y: int
dx: int
dy: int
robot_spec_pattern = re.compile(r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)")
class DayFourteenSolver(Solver):
robots: list[Robot]
width: int
height: int
@override
def __init__(self, puzzle_input: str, width=101, height=103):
self.robots = [
Robot(*map(int, match.groups()))
for match in robot_spec_pattern.finditer(puzzle_input)
]
self.width = width
self.height = height
@override
def solve_p1(self) -> int:
quadrant_counts = Counter(
filter_map(
lambda r: self.get_quadrant(self.move_robot(r, 100)),
self.robots,
)
)
return prod(quadrant_counts.values())
@override
def solve_p2(self) -> int:
state = iterate(
lambda robots: [self.move_robot(robot, 1) for robot in robots],
self.robots,
)
return first_true(
count(),
pred=lambda i: self.total_overlap(next(state)) == 0,
)
def move_robot(self, robot: Robot, seconds: int) -> Robot:
return Robot(
(robot.x + robot.dx * seconds) % self.width,
(robot.y + robot.dy * seconds) % self.height,
robot.dx,
robot.dy,
)
def get_quadrant(self, robot: Robot) -> int | None:
if robot.x > self.width // 2 and robot.y < self.height // 2:
return 1
if robot.x < self.width // 2 and robot.y < self.height // 2:
return 2
if robot.x < self.width // 2 and robot.y > self.height // 2:
return 3
if robot.x > self.width // 2 and robot.y > self.height // 2:
return 4
return None
def total_overlap(self, robots: list[Robot]) -> int:
locations = Counter((robot.x, robot.y) for robot in robots)
return sum(count for count in locations.values() if count > 1)
class TestDayFourteenSolver(TestCase):
def test(self):
solver = DayFourteenSolver(
dedent(
"""
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
"""
),
width=11,
height=7,
)
self.assertTupleEqual(
solver.robots[0],
Robot(0, 4, 3, -3),
)
self.assertTupleEqual(
solver.move_robot(solver.robots[0], 1),
Robot(3, 1, 3, -3),
)
self.assertEqual(solver.get_quadrant(Robot(0, 0, 0, 0)), 2)
self.assertEqual(solver.get_quadrant(Robot(5, 0, 0, 0)), None)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(0, 0, 0, 0),
]
),
2,
)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(1, 0, 0, 0),
]
),
0,
)
self.assertEqual(solver.solve_p1(), 12)

View File

@@ -1,5 +1,5 @@
from itertools import count, pairwise
import math
from itertools import count, pairwise
from typing import Iterable, NamedTuple, override
from unittest import TestCase