Skip to content

Commit d4a2d80

Browse files
committed
Day 14 solution
1 parent 377dcfe commit d4a2d80

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

solutions/day14.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from typing import List, NamedTuple, Set, Tuple
2+
3+
from aoc.models.base import SolutionBase
4+
5+
6+
class Position(NamedTuple):
7+
"""Find first time when robots collide.
8+
9+
Simulates robot movement until two or more robots occupy the same position,
10+
indicating a collision.
11+
12+
Args:
13+
data: List of strings containing robot configurations
14+
15+
Returns:
16+
Time step when the first collision occurs
17+
"""
18+
19+
x: int
20+
y: int
21+
22+
23+
class Robot(NamedTuple):
24+
"""Represents a robot with its current position and velocity.
25+
26+
Attributes:
27+
pos: Position namedtuple containing the robot's current (x, y) coordinates
28+
velocity: Position namedtuple containing the robot's (dx, dy) velocity components
29+
"""
30+
31+
pos: Position
32+
velocity: Position
33+
34+
35+
class Solution(SolutionBase):
36+
"""Solution for Advent of Code 2024 - Day 14: Restroom Redoubt.
37+
38+
This class solves a puzzle involving robots moving in a confined grid space,
39+
where each robot has an initial position and velocity. The robots move in a
40+
wrapping grid pattern, and their positions must be tracked over time to solve
41+
various spatial puzzles.
42+
43+
Input format:
44+
List of strings where each line represents a robot with:
45+
- Initial position (p=x,y)
46+
- Initial velocity (V=x,y)
47+
Example: "p=0,4 v=3,-3"
48+
49+
The solution uses custom Position and Robot namedtuples to track robot states
50+
and implements methods to calculate robot positions over time in a wrapping
51+
grid space.
52+
"""
53+
54+
def parse_data(self, data: List[str]) -> List[Robot]:
55+
"""Parse input data into a list of Robot objects.
56+
57+
Args:
58+
data: List of strings containing robot position and velocity data
59+
60+
Returns:
61+
List of Robot objects with initial positions and velocities
62+
"""
63+
robots = []
64+
for line in data:
65+
pos_str, vel_str = line.split(" ")
66+
x, y = map(int, pos_str[2:].split(","))
67+
velocity_x, velocity_y = map(int, vel_str[2:].split(","))
68+
robots.append(Robot(Position(x, y), Position(velocity_x, velocity_y)))
69+
70+
return robots
71+
72+
def get_grid_size(self, robots: List[Robot]) -> Tuple[int, int]:
73+
"""Determine the size of the grid based on number of robots.
74+
75+
Different grid sizes are used for the sample input (12 robots)
76+
versus the actual puzzle input.
77+
78+
Args:
79+
robots: List of Robot objects
80+
81+
Returns:
82+
Tuple of (width, height) representing grid dimensions
83+
"""
84+
return (101, 103) if len(robots) != 12 else (11, 7)
85+
86+
def get_position_at_time(self, robot: Robot, time: int, width: int, height: int) -> Position:
87+
"""Calculate a robot's position after a given amount of time.
88+
89+
Handles wrapping movement where robots that move beyond grid boundaries
90+
appear on the opposite side.
91+
92+
Args:
93+
robot: Robot object with initial position and velocity
94+
time: Number of time steps to simulate
95+
width: Grid width
96+
height: Grid height
97+
98+
Returns:
99+
Position representing robot's location after specified time
100+
"""
101+
x = (robot.pos.x + time * (robot.velocity.x + width)) % width
102+
y = (robot.pos.y + time * (robot.velocity.y + height)) % height
103+
return Position(x, y)
104+
105+
def part1(self, data: List[str]) -> int:
106+
"""Calculate product of robots in each quadrant after 100 time steps.
107+
108+
Divides the grid into four quadrants and counts robots in each,
109+
excluding robots on the center lines. Returns the product of these counts.
110+
111+
Args:
112+
data: List of strings containing robot configurations
113+
114+
Returns:
115+
Product of robot counts in each quadrant
116+
"""
117+
robots = self.parse_data(data)
118+
width, height = self.get_grid_size(robots)
119+
120+
quads = [0] * 4
121+
122+
for robot in robots:
123+
position = self.get_position_at_time(robot, 100, width, height)
124+
125+
if position.x == width // 2 or position.y == height // 2:
126+
continue
127+
128+
quad_idx = (int(position.x > width // 2)) + (int(position.y > height // 2) * 2)
129+
quads[quad_idx] += 1
130+
131+
return quads[0] * quads[1] * quads[2] * quads[3]
132+
133+
def part2(self, data: List[str]) -> int:
134+
"""Find first time when robots collide.
135+
136+
Simulates robot movement until two or more robots occupy the same position,
137+
indicating a collision.
138+
139+
Args:
140+
data: List of strings containing robot configurations
141+
142+
Returns:
143+
Time step when the first collision occurs
144+
"""
145+
robots = self.parse_data(data)
146+
width, height = self.get_grid_size(robots)
147+
148+
time = 0
149+
while True:
150+
time += 1
151+
position: Set[Position] = set()
152+
153+
for robot in robots:
154+
new_pos = self.get_position_at_time(robot, time, width, height)
155+
if new_pos in position:
156+
break
157+
158+
position.add(new_pos)
159+
160+
else: # no breaks occurred - valid solution found
161+
return time

0 commit comments

Comments
 (0)