Skip to content

Commit edad87f

Browse files
committed
Day 13 solution
1 parent 3778c89 commit edad87f

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed

solutions/day13.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import re
2+
from typing import List, Tuple
3+
4+
from aoc.models.base import SolutionBase
5+
6+
7+
class Solution(SolutionBase):
8+
"""Solution for Advent of Code 2024 - Day 13: Claw Contraption.
9+
10+
This class solves a puzzle involving a claw machine game where two buttons control
11+
the movement of a claw. Each button press moves the claw by specific X and Y
12+
coordinates, and the goal is to reach prize locations using the minimum number
13+
of button presses.
14+
15+
Input format:
16+
Groups of three lines representing claw machines, where each group contains:
17+
- Button A's movement coordinates (X+n, Y+n)
18+
- Button B's movement coordinates (X+n, Y+n)
19+
- Prize coordinates (X=n, Y=n)
20+
21+
This class inherits from `SolutionBase` and provides methods to parse machine
22+
configurations, calculate required button presses using Cramer's rule, and
23+
determine the minimum number of tokens needed to win prizes.
24+
"""
25+
26+
button_regex = r"Button [AB]: X\+(\d+), Y\+(\d+)"
27+
prize_regex = r"Prize: X=(\d+), Y=(\d+)"
28+
OFFSET = 10000000000000
29+
30+
def extract(self, coord: str, ext_type: str) -> Tuple[int, int]:
31+
"""Extract X and Y coordinates from button or prize position strings.
32+
33+
Args:
34+
coord: String containing coordinate information
35+
ext_type: Type of coordinates to extract ("button" or "prize")
36+
37+
Returns:
38+
Tuple of (x, y) coordinates extracted from the string
39+
"""
40+
pattern = self.button_regex if ext_type == "button" else self.prize_regex
41+
return tuple(map(int, re.findall(pattern, coord)[0]))
42+
43+
def solve_machine(
44+
self,
45+
btn_a: Tuple[int, int],
46+
btn_b: Tuple[int, int],
47+
prize: Tuple[int, int],
48+
limit: int | None = 100,
49+
) -> int | None:
50+
"""Calculate minimum tokens needed to reach prize coordinates using button presses.
51+
52+
Uses Cramer's rule to solve the system of linear equations that determine
53+
how many times each button needs to be pressed to reach the prize coordinates.
54+
55+
Args:
56+
btn_a: Tuple of (x, y) movement per Button A press
57+
btn_b: Tuple of (x, y) movement per Button B press
58+
prize: Tuple of (x, y) target prize coordinates
59+
limit: Maximum allowed button presses (None for unlimited)
60+
61+
Returns:
62+
Total tokens needed (3 per Button A press + 1 per Button B press),
63+
or None if no valid solution exists
64+
"""
65+
denominator = btn_b[1] * btn_a[0] - btn_b[0] * btn_a[1]
66+
if denominator == 0:
67+
return None
68+
69+
times_b = (prize[1] * btn_a[0] - prize[0] * btn_a[1]) / denominator
70+
times_a = (prize[0] - btn_b[0] * times_b) / btn_a[0]
71+
72+
if times_a.is_integer() and times_b.is_integer():
73+
if limit is None or (0 <= times_a <= limit and 0 <= times_b <= limit):
74+
return int(times_a) * 3 + int(times_b)
75+
76+
return None
77+
78+
def calculate_coins(self, data: List[str], part2: bool = False) -> int:
79+
"""Process all claw machines and calculate total tokens needed.
80+
81+
Parses input data into individual machine configurations and calculates
82+
the minimum tokens needed for each winnable prize.
83+
84+
Args:
85+
data: List of strings containing machine configurations
86+
part2: Whether to apply the large coordinate offset for part 2
87+
88+
Returns:
89+
Total tokens needed to win all possible prizes
90+
"""
91+
machines = ("\n".join(data)).split("\n\n")
92+
coins = 0
93+
94+
for machine in machines:
95+
btn_a, btn_b, prize = machine.split("\n")
96+
97+
btn_a_coords = self.extract(btn_a, "button")
98+
btn_b_coords = self.extract(btn_b, "button")
99+
prize_coords = self.extract(prize, "prize")
100+
101+
if part2:
102+
prize_coords = (prize_coords[0] + self.OFFSET, prize_coords[1] + self.OFFSET)
103+
104+
if result := self.solve_machine(
105+
btn_a_coords, btn_b_coords, prize_coords, None if part2 else 100
106+
):
107+
coins += result
108+
109+
return coins
110+
111+
def part1(self, data: List[str]) -> int:
112+
"""Calculate minimum tokens needed with standard prize coordinates.
113+
114+
Args:
115+
data: List of strings containing machine configurations
116+
117+
Returns:
118+
Total tokens needed when each button press is limited to 100
119+
"""
120+
return self.calculate_coins(data)
121+
122+
def part2(self, data: List[str]) -> int:
123+
"""Calculate minimum tokens needed with offset prize coordinates.
124+
125+
Args:
126+
data: List of strings containing machine configurations
127+
128+
Returns:
129+
Total tokens needed when prizes are offset by 10^13 units
130+
"""
131+
return self.calculate_coins(data, True)

0 commit comments

Comments
 (0)