Skip to content

Commit 081020c

Browse files
committed
2024 day 15 part 2
1 parent 97da4b8 commit 081020c

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- 2021 - ★★★★★ ★★★★★ ★★★★★ ★★★
1616
- 2022 - ★★★★★ ★★★★★ ★★★★★ ★☆
1717
- 2023 - ★★★★★ ★★★★★ ★★★★★ ★★★☆
18-
- 2024 - ★★★★★ ★★★★★ ★★★★
18+
- 2024 - ★★★★★ ★★★★★ ★★★★
1919

2020
## How to use
2121

src/year2024/day15b.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""2024 - Day 15 Part 2: Warehouse Woes"""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC
6+
from abc import abstractmethod
7+
from dataclasses import dataclass
8+
from enum import StrEnum
9+
10+
11+
class Instruction(StrEnum):
12+
NORTH = "^"
13+
EAST = ">"
14+
SOUTH = "v"
15+
WEST = "<"
16+
17+
18+
SHIFTS = {
19+
# dr, dc
20+
Instruction.NORTH: (-1, 0),
21+
Instruction.EAST: (0, +1),
22+
Instruction.SOUTH: (+1, 0),
23+
Instruction.WEST: (0, -1),
24+
}
25+
26+
27+
@dataclass
28+
class Object(ABC):
29+
symbol: str
30+
r: int
31+
c: int
32+
33+
@abstractmethod
34+
def can_move(self, instruction: Instruction, warehouse: Warehouse) -> bool:
35+
raise NotImplementedError
36+
37+
@abstractmethod
38+
def move(self, instruction: Instruction, warehouse: Warehouse) -> None:
39+
raise NotImplementedError
40+
41+
@abstractmethod
42+
def update_position(self, nr: int, nc: int, warehouse: Warehouse) -> None:
43+
raise NotImplementedError
44+
45+
46+
class Bulky(Object, ABC):
47+
def can_move(self, instruction: Instruction, warehouse: Warehouse) -> bool:
48+
moveable = True
49+
50+
dr, dc = SHIFTS[instruction]
51+
nr, nc = self.r + dr, self.c + dc
52+
53+
if (nr, nc) in warehouse.objects:
54+
neighbor1 = warehouse.objects[(nr, nc)]
55+
moveable &= neighbor1 is self or neighbor1.can_move(
56+
instruction, warehouse
57+
)
58+
if (nr, nc + 1) in warehouse.objects:
59+
neighbor2 = warehouse.objects[(nr, nc + 1)]
60+
moveable &= neighbor2 is self or neighbor2.can_move(
61+
instruction, warehouse
62+
)
63+
64+
return moveable
65+
66+
def move(self, instruction: Instruction, warehouse: Warehouse) -> None:
67+
if self.can_move(instruction, warehouse):
68+
dr, dc = SHIFTS[instruction]
69+
nr, nc = self.r + dr, self.c + dc
70+
71+
if (nr, nc) in warehouse.objects:
72+
neighbor = warehouse.objects[(nr, nc)]
73+
if neighbor is not self:
74+
neighbor.move(instruction, warehouse)
75+
76+
if (nr, nc + 1) in warehouse.objects:
77+
neighbor = warehouse.objects[(nr, nc + 1)]
78+
if neighbor is not self:
79+
neighbor.move(instruction, warehouse)
80+
81+
self.update_position(nr, nc, warehouse)
82+
83+
def update_position(self, nr: int, nc: int, warehouse: Warehouse) -> None:
84+
del warehouse.objects[(self.r, self.c)]
85+
del warehouse.objects[(self.r, self.c + 1)]
86+
87+
self.r, self.c = nr, nc
88+
89+
warehouse.objects[(nr, nc)] = self
90+
warehouse.objects[(nr, nc + 1)] = self
91+
92+
93+
class Wall(Bulky):
94+
def can_move(self, instruction: Instruction, warehouse: Warehouse) -> bool:
95+
return False
96+
97+
def move(self, instruction: Instruction, warehouse: Warehouse) -> None: ...
98+
99+
100+
class Box(Bulky):
101+
@property
102+
def gps(self) -> int:
103+
return self.r * 100 + self.c
104+
105+
106+
class Robot(Object):
107+
def can_move(self, instruction: Instruction, warehouse: Warehouse) -> bool:
108+
dr, dc = SHIFTS[instruction]
109+
nr, nc = self.r + dr, self.c + dc
110+
111+
if (nr, nc) in warehouse.objects:
112+
neighbor = warehouse.objects[(nr, nc)]
113+
return neighbor.can_move(instruction, warehouse)
114+
else:
115+
return True
116+
117+
def move(self, instruction: Instruction, warehouse: Warehouse) -> None:
118+
if self.can_move(instruction, warehouse):
119+
dr, dc = SHIFTS[instruction]
120+
nr, nc = self.r + dr, self.c + dc
121+
if (nr, nc) in warehouse.objects:
122+
neighbor = warehouse.objects[(nr, nc)]
123+
neighbor.move(instruction, warehouse)
124+
self.update_position(nr, nc, warehouse)
125+
126+
def update_position(self, nr: int, nc: int, warehouse: Warehouse) -> None:
127+
del warehouse.objects[(self.r, self.c)]
128+
self.r, self.c = nr, nc
129+
warehouse.objects[(nr, nc)] = self
130+
131+
132+
@dataclass
133+
class Warehouse:
134+
height: int
135+
width: int
136+
robot: Robot
137+
objects: dict[tuple[int, int], Object]
138+
boxes: list[Box]
139+
140+
@classmethod
141+
def from_text(cls, text: str) -> Warehouse:
142+
robot = Robot(r=0, c=0, symbol="@")
143+
objects: dict[tuple[int, int], Object] = {}
144+
boxes: list[Box] = []
145+
146+
lines = text.split("\n")
147+
height, width = len(lines), len(lines[0] * 2)
148+
149+
for r, row in enumerate(lines):
150+
for c, x in enumerate(row):
151+
if x == "#":
152+
wall = Wall("#", r, c * 2)
153+
objects[(r, c * 2)] = wall
154+
objects[(r, c * 2 + 1)] = wall
155+
elif x == "@":
156+
robot.r, robot.c = r, c * 2
157+
objects[(r, c * 2)] = robot
158+
elif x == "O":
159+
box = Box("O", r, c * 2)
160+
boxes.append(box)
161+
objects[(r, c * 2)] = box
162+
objects[(r, c * 2 + 1)] = box
163+
164+
return cls(height, width, robot, objects, boxes)
165+
166+
def move_robot(self, instructions: list[Instruction]) -> None:
167+
for instruction in instructions:
168+
self.robot.move(instruction, self)
169+
170+
@property
171+
def gps_sum(self) -> int:
172+
return sum(box.gps for box in self.boxes)
173+
174+
def __str__(self) -> str:
175+
lines = [["."] * self.width for _ in range(self.height)]
176+
177+
for (r, c), v in self.objects.items():
178+
lines[r][c] = v.symbol
179+
180+
return "\n".join("".join(line) for line in lines)
181+
182+
183+
def process_data(task: str) -> tuple[Warehouse, list[Instruction]]:
184+
first, second = task.split("\n\n")
185+
warehouse = Warehouse.from_text(first)
186+
instructions = [Instruction(x) for x in second if x in Instruction]
187+
return warehouse, instructions
188+
189+
190+
def solve(task: str) -> int:
191+
warehouse, instructions = process_data(task)
192+
warehouse.move_robot(instructions)
193+
return warehouse.gps_sum

tests/src/year2024/test_day15b.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""2024 - Day 15 Part 2: Warehouse Woes"""
2+
3+
from textwrap import dedent
4+
5+
from src.year2024.day15b import solve
6+
7+
8+
def test_solve_large():
9+
task = dedent(
10+
"""
11+
##########
12+
#..O..O.O#
13+
#......O.#
14+
#.OO..O.O#
15+
#..O@..O.#
16+
#O#..O...#
17+
#O..O..O.#
18+
#.OO.O.OO#
19+
#....O...#
20+
##########
21+
22+
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
23+
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
24+
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
25+
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
26+
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
27+
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
28+
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
29+
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
30+
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
31+
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^
32+
"""
33+
).strip()
34+
assert solve(task) == 9021

0 commit comments

Comments
 (0)