Skip to content

Commit 7201698

Browse files
committed
Merge branch 'day17'
2 parents fb785a6 + 0cffad2 commit 7201698

File tree

8 files changed

+455
-18
lines changed

8 files changed

+455
-18
lines changed

.run/Python tests in part1.py.run.xml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
<module name="aoc2022"/>
55
<option name="INTERPRETER_OPTIONS" value=""/>
66
<option name="PARENT_ENVS" value="true"/>
7-
<option name="SDK_HOME" value="D:\DEV\Python\Projects\aoc2022\venv\Scripts\python.exe"/>
8-
<option name="SDK_NAME" value="Python 3.10 (aoc2022)"/>
9-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day16"/>
10-
<option name="IS_MODULE_SDK" value="false"/>
7+
<option name="SDK_HOME" value=""/>
8+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day17"/>
9+
<option name="IS_MODULE_SDK" value="true"/>
1110
<option name="ADD_CONTENT_ROOTS" value="true"/>
1211
<option name="ADD_SOURCE_ROOTS" value="true"/>
1312
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
1413
<option name="_new_additionalArguments" value="&quot;-s&quot;"/>
15-
<option name="_new_target" value="&quot;$PROJECT_DIR$/day16/part1.py&quot;"/>
14+
<option name="_new_target" value="&quot;$PROJECT_DIR$/day17/part1.py&quot;"/>
1615
<option name="_new_targetType" value="&quot;PATH&quot;"/>
1716
<method v="2"/>
1817
</configuration>

.run/Python tests in part2.py.run.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
<module name="aoc2022"/>
44
<option name="INTERPRETER_OPTIONS" value=""/>
55
<option name="PARENT_ENVS" value="true"/>
6-
<option name="SDK_HOME" value="D:\DEV\Python\Projects\aoc2022\venv\Scripts\python.exe"/>
6+
<option name="SDK_HOME" value="D:\DEV\Python\Projects\aoc2022\.venv\Scripts\python.exe"/>
77
<option name="SDK_NAME" value="Python 3.10 (aoc2022)"/>
8-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day16"/>
8+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day17"/>
99
<option name="IS_MODULE_SDK" value="false"/>
1010
<option name="ADD_CONTENT_ROOTS" value="true"/>
1111
<option name="ADD_SOURCE_ROOTS" value="true"/>
1212
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
1313
<option name="_new_additionalArguments" value="&quot;-s&quot;"/>
14-
<option name="_new_target" value="&quot;$PROJECT_DIR$/day16/part2.py&quot;"/>
14+
<option name="_new_target" value="&quot;$PROJECT_DIR$/day17/part2.py&quot;"/>
1515
<option name="_new_targetType" value="&quot;PATH&quot;"/>
1616
<method v="2"/>
1717
</configuration>

.run/part1.run.xml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
<envs>
77
<env name="PYTHONUNBUFFERED" value="1"/>
88
</envs>
9-
<option name="SDK_HOME" value="D:\DEV\Python\Projects\aoc2022\venv\Scripts\python.exe"/>
10-
<option name="SDK_NAME" value="Python 3.10 (aoc2022)"/>
11-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day16"/>
12-
<option name="IS_MODULE_SDK" value="false"/>
9+
<option name="SDK_HOME" value=""/>
10+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day17"/>
11+
<option name="IS_MODULE_SDK" value="true"/>
1312
<option name="ADD_CONTENT_ROOTS" value="true"/>
1413
<option name="ADD_SOURCE_ROOTS" value="true"/>
1514
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
16-
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/day16/part1.py"/>
15+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/day17/part1.py"/>
1716
<option name="PARAMETERS" value="input.txt"/>
1817
<option name="SHOW_COMMAND_LINE" value="false"/>
1918
<option name="EMULATE_TERMINAL" value="false"/>

.run/part2.run.xml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
<envs>
77
<env name="PYTHONUNBUFFERED" value="1"/>
88
</envs>
9-
<option name="SDK_HOME" value="D:\DEV\Python\Projects\aoc2022\venv\Scripts\python.exe"/>
10-
<option name="SDK_NAME" value="Python 3.10 (aoc2022)"/>
11-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day16"/>
12-
<option name="IS_MODULE_SDK" value="false"/>
9+
<option name="SDK_HOME" value=""/>
10+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/day17"/>
11+
<option name="IS_MODULE_SDK" value="true"/>
1312
<option name="ADD_CONTENT_ROOTS" value="true"/>
1413
<option name="ADD_SOURCE_ROOTS" value="true"/>
1514
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
16-
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/day16/part2.py"/>
15+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/day17/part2.py"/>
1716
<option name="PARAMETERS" value="input.txt"/>
1817
<option name="SHOW_COMMAND_LINE" value="false"/>
1918
<option name="EMULATE_TERMINAL" value="false"/>

day17/__init__.py

Whitespace-only changes.

day17/part1.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import argparse
2+
import itertools
3+
import os.path
4+
import textwrap
5+
from copy import deepcopy
6+
from dataclasses import dataclass
7+
from typing import ClassVar
8+
9+
import pytest
10+
11+
from support import Direction4
12+
from support import timing
13+
14+
INPUT_TXT = os.path.join(os.path.dirname(__file__), 'input.txt')
15+
16+
# NOTE: paste test text here
17+
INPUT_S = '''\
18+
>>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>
19+
'''
20+
EXPECTED = 3068
21+
22+
Coord = tuple[int, int]
23+
24+
25+
@dataclass
26+
class Rock:
27+
SHAPES_STR: ClassVar[str] = textwrap.dedent("""\
28+
####
29+
30+
.#.
31+
###
32+
.#.
33+
34+
..#
35+
..#
36+
###
37+
38+
#
39+
#
40+
#
41+
#
42+
43+
##
44+
##
45+
""")
46+
name: str
47+
shape: set[Coord]
48+
49+
@classmethod
50+
def from_str(cls, name, s):
51+
"""Load the rock coords from string.
52+
53+
Must load the coords "bottom up".
54+
"""
55+
coords = set()
56+
for y, line in enumerate(reversed(s.splitlines())):
57+
for x, c in enumerate(line):
58+
if c == '#':
59+
coords.add((x, y))
60+
return cls(name, coords)
61+
62+
def move(self, _dir: Direction4, n=1):
63+
self.shape = {_dir.apply(*coord, n=n) for coord in self}
64+
65+
def fall(self):
66+
"""Direction is reversed due to the up-down flip."""
67+
self.shape = {Direction4.UP.apply(*coord) for coord in self}
68+
69+
def __iter__(self):
70+
yield from self.shape
71+
72+
def __str__(self):
73+
return f'Rock({self.name})'
74+
75+
__repr__ = __str__
76+
77+
78+
class Chamber:
79+
jet_to_dir = {
80+
'<': Direction4.LEFT,
81+
'>': Direction4.RIGHT,
82+
}
83+
84+
def __init__(self, jets: str, rocks: list[Rock]):
85+
self.jets = itertools.cycle(jets)
86+
self.rocks = itertools.cycle(rocks)
87+
self.occupied: set[Coord] = set()
88+
self.rock = None
89+
90+
@property
91+
def max_height(self):
92+
if not self.occupied:
93+
return 0
94+
return max(c[1] for c in self.occupied)
95+
96+
def collision(self, rock: Rock):
97+
in_wall = any(c[0] in [0, 8] for c in rock)
98+
on_the_floor = any(c[1] == 0 for c in rock)
99+
in_occupied = any(c in self.occupied for c in rock)
100+
return any((in_wall, on_the_floor, in_occupied))
101+
102+
def spawn_rock(self, rock):
103+
rock = deepcopy(rock)
104+
rock.move(Direction4.DOWN, n=self.max_height + 1 + 3)
105+
rock.move(Direction4.RIGHT, n=3)
106+
self.rock = rock
107+
108+
def process_rock(self):
109+
"""Repeat following.
110+
111+
- spawn rock
112+
- move
113+
- fall
114+
"""
115+
rock = next(self.rocks)
116+
self.spawn_rock(rock)
117+
118+
while True:
119+
_dir = self.jet_to_dir[next(self.jets)]
120+
121+
# move the rock
122+
self.rock.move(_dir)
123+
if self.collision(self.rock):
124+
# if collision, revert
125+
self.rock.move(_dir.opposite)
126+
127+
# fall the rock
128+
self.rock.fall()
129+
if self.collision(self.rock):
130+
# collision during the fall, freeze and continue
131+
self.rock.move(Direction4.DOWN)
132+
self.occupied |= self.rock.shape
133+
break
134+
135+
def _coord_to_char(self, c):
136+
if c in self.occupied:
137+
return '#'
138+
if self.rock and c in self.rock:
139+
return '@'
140+
return '.'
141+
142+
def __str__(self):
143+
print_height = self.max_height + 8
144+
rows = []
145+
for y in range(print_height, 0, -1):
146+
row = str(y) + ' |' + ''.join(self._coord_to_char((x, y)) for x in range(1, 8)) + '|'
147+
rows.append(row)
148+
rows.append('0 +-------+')
149+
return '\n'.join(rows)
150+
151+
152+
def compute(s: str) -> int:
153+
jets: str = s.strip()
154+
shapes: list[str] = Rock.SHAPES_STR.split('\n\n')
155+
names: str = '-+jio'
156+
rocks = [Rock.from_str(n, s) for n, s in zip(names, shapes)]
157+
chamber = Chamber(jets, rocks)
158+
159+
for _ in range(2022):
160+
chamber.process_rock()
161+
# print(chamber)
162+
163+
return chamber.max_height
164+
165+
166+
@pytest.mark.solved
167+
@pytest.mark.parametrize(
168+
('input_s', 'expected'),
169+
(
170+
(INPUT_S, EXPECTED),
171+
),
172+
)
173+
def test(input_s: str, expected: int) -> None:
174+
print() # newline in test output, helps readability
175+
assert compute(input_s) == expected
176+
177+
178+
def main() -> int:
179+
parser = argparse.ArgumentParser()
180+
parser.add_argument('data_file', nargs='?', default=INPUT_TXT)
181+
args = parser.parse_args()
182+
183+
with open(args.data_file) as f, timing():
184+
print(compute(f.read()))
185+
186+
return 0
187+
188+
189+
if __name__ == '__main__':
190+
raise SystemExit(main())

0 commit comments

Comments
 (0)