Skip to content

Commit c086762

Browse files
committed
Added 2020-20 and new features in grid
1 parent 749b8e5 commit c086762

File tree

2 files changed

+525
-13
lines changed

2 files changed

+525
-13
lines changed

2020/20-Jurassic Jigsaw.py

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# -------------------------------- Input data ---------------------------------------- #
2+
import os, grid, graph, dot, assembly, re, itertools, math
3+
from collections import Counter, deque, defaultdict
4+
5+
from functools import reduce
6+
from compass import *
7+
8+
# This functions come from https://github.com/mcpower/adventofcode - Thanks!
9+
def lmap(func, *iterables):
10+
return list(map(func, *iterables))
11+
12+
13+
def ints(s: str):
14+
return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano!
15+
16+
17+
def positive_ints(s: str):
18+
return lmap(int, re.findall(r"\d+", s)) # thanks mserrano!
19+
20+
21+
def floats(s: str):
22+
return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s))
23+
24+
25+
def positive_floats(s: str):
26+
return lmap(float, re.findall(r"\d+(?:\.\d+)?", s))
27+
28+
29+
def words(s: str):
30+
return re.findall(r"[a-zA-Z]+", s)
31+
32+
33+
test_data = {}
34+
35+
test = 1
36+
test_data[test] = {
37+
"input": """Tile 1:
38+
A-B
39+
| |
40+
D-C
41+
42+
Tile 2:
43+
C-D
44+
| |
45+
B-A,
46+
47+
Tile 3:
48+
X-Y
49+
| |
50+
B-A""",
51+
"expected": ["""""", "Unknown"],
52+
}
53+
54+
test += 1
55+
input_file = os.path.join(
56+
os.path.dirname(__file__),
57+
"Inputs",
58+
os.path.basename(__file__).replace(".py", "-sample.txt"),
59+
)
60+
test_data[test] = {
61+
"input": open(input_file, "r+").read(),
62+
"expected": ["""20899048083289""", "273"],
63+
}
64+
65+
test = "real"
66+
input_file = os.path.join(
67+
os.path.dirname(__file__),
68+
"Inputs",
69+
os.path.basename(__file__).replace(".py", ".txt"),
70+
)
71+
test_data[test] = {
72+
"input": open(input_file, "r+").read(),
73+
"expected": ["54755174472007", "1692"],
74+
}
75+
76+
77+
# -------------------------------- Control program execution ------------------------- #
78+
79+
case_to_test = "real"
80+
part_to_test = 2
81+
82+
# -------------------------------- Initialize some variables ------------------------- #
83+
84+
puzzle_input = test_data[case_to_test]["input"]
85+
puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1]
86+
puzzle_actual_result = "Unknown"
87+
88+
89+
# -------------------------------- Actual code execution ----------------------------- #
90+
def matches(cam1, cam2):
91+
if isinstance(cam1, int):
92+
cam1 = set().union(*(cam_borders[cam1].values()))
93+
if isinstance(cam2, int):
94+
cam2 = set().union(*(cam_borders[cam2].values()))
95+
if isinstance(cam1, str):
96+
cam1 = {cam1}
97+
if isinstance(cam2, str):
98+
cam2 = {cam2}
99+
100+
return [border for border in cam1 if border in cam2]
101+
102+
103+
def nb_matches(cam1, cam2):
104+
return len(matches(cam1, cam2))
105+
106+
107+
# This looks for the best orientation of a specific camera, based on its position
108+
# It's possible to filter by angles & by neighbors
109+
def find_best_orientation(cam1, position, possible_neighbors=[]):
110+
# If cam1 is provided as camera number, select all angles
111+
if isinstance(cam1, int):
112+
cam1 = [(cam1, angle1) for angle1 in all_angles]
113+
# If possible neighbors not provided, get them from neighbors
114+
if possible_neighbors == []:
115+
possible_neighbors = [cam2 for c1 in cam1 for cam2 in neighbors[c1]]
116+
117+
angles = defaultdict(list)
118+
best_angle = 0
119+
# By looking through all the orientations of cam1 + neighbors, determine all possible combinations
120+
for (cid1, angle1) in cam1:
121+
borders1 = cam_borders[cid1][angle1]
122+
for (cid2, angle2) in possible_neighbors:
123+
cam2 = cam_borders[cid2]
124+
borders2 = cam2[angle2]
125+
for offset, touchpoint in offset_to_border.items():
126+
# Let's put that corner in top left
127+
if (position + offset).imag > 0 or (position + offset).real < 0:
128+
continue
129+
if borders1[touchpoint[0]] == borders2[touchpoint[1]]:
130+
angles[angle1].append((cid2, angle2, offset))
131+
132+
if len(angles.values()) == 0:
133+
return False
134+
135+
best_angle = max([len(angle) for angle in angles.values()])
136+
137+
return {
138+
angle: angles[angle] for angle in angles if len(angles[angle]) == best_angle
139+
}
140+
141+
142+
# There are all the relevant "angles" (actually operations) we can do
143+
# Normal
144+
# Normal + flip vertical
145+
# Normal + flip horizontal
146+
# Rotated 90°
147+
# Rotated 90° + flip vertical
148+
# Rotated 90° + flip horizontal
149+
# Rotated 180°
150+
# Rotated 270°
151+
# Flipping the 180° or 270° would give same results as before
152+
all_angles = [
153+
(0, "N"),
154+
(0, "V"),
155+
(0, "H"),
156+
(90, "N"),
157+
(90, "V"),
158+
(90, "H"),
159+
(180, "N"),
160+
(270, "N"),
161+
]
162+
163+
164+
cam_borders = {}
165+
cam_image = {}
166+
cam_size = len(puzzle_input.split("\n\n")[0].split("\n")[1])
167+
for camera in puzzle_input.split("\n\n"):
168+
camera_id = ints(camera.split("\n")[0])[0]
169+
image = grid.Grid()
170+
image.text_to_dots("\n".join(camera.split("\n")[1:]))
171+
cam_image[camera_id] = image
172+
173+
borders = {}
174+
for orientation in all_angles:
175+
new_image = image.flip(orientation[1])[0].rotate(orientation[0])[0]
176+
borders.update({orientation: new_image.get_borders()})
177+
178+
cam_borders[camera_id] = borders
179+
180+
match = {}
181+
for camera_id, camera in cam_borders.items():
182+
value = (
183+
sum(
184+
[
185+
nb_matches(camera_id, other_cam)
186+
for other_cam in cam_borders
187+
if other_cam != camera_id
188+
]
189+
)
190+
// 2
191+
) # Each match is counted twice because borders get flipped and still match
192+
match[camera_id] = value
193+
194+
corners = [cid for cid in cam_borders if match[cid] == 2]
195+
196+
if part_to_test == 1:
197+
puzzle_actual_result = reduce(lambda x, y: x * y, corners)
198+
199+
else:
200+
# This reads as:
201+
# Cam2 is north of cam1: cam1's border 0 must match cam2's border 2
202+
offset_to_border = {north: (0, 2), east: (1, 3), south: (2, 0), west: (3, 1)}
203+
204+
# This is the map of the possible neighbors
205+
neighbors = {
206+
(cid1, angle1): {
207+
(cid2, angle2)
208+
for cid2 in cam_borders
209+
for angle2 in all_angles
210+
if cid1 != cid2
211+
and nb_matches(cam_borders[cid1][angle1], cam_borders[cid2][angle2]) > 0
212+
}
213+
for cid1 in cam_borders
214+
for angle1 in all_angles
215+
}
216+
217+
# First, let's choose a corner
218+
cam = corners[0]
219+
image_pieces = {}
220+
221+
# Then, let's determine its orientation & find some neighbors
222+
angles = find_best_orientation(cam, 0)
223+
possible_angles = {
224+
x: angles[x]
225+
for x in angles
226+
if all([n[2].real >= 0 and n[2].imag <= 0 for n in angles[x]])
227+
}
228+
# There should be 2 options (one transposed from the other), so we choose one
229+
# Since the whole image will get flipped anyway, it has no impact
230+
chosen_angle = list(possible_angles.keys())[0]
231+
image_pieces[0] = (cam, chosen_angle)
232+
image_pieces[angles[chosen_angle][0][2]] = angles[chosen_angle][0][:2]
233+
image_pieces[angles[chosen_angle][1][2]] = angles[chosen_angle][1][:2]
234+
235+
del angles, possible_angles, chosen_angle
236+
237+
# Find all other pieces
238+
grid_size = int(math.sqrt(len(cam_image)))
239+
for x in range(grid_size):
240+
for y in range(grid_size):
241+
cam_pos = x - 1j * y
242+
if cam_pos in image_pieces:
243+
continue
244+
245+
# Which neighbors do we already have?
246+
neigh_offset = list(
247+
dir for dir in directions_straight if cam_pos + dir in image_pieces
248+
)
249+
neigh_vals = [image_pieces[cam_pos + dir] for dir in neigh_offset]
250+
251+
# Based on the neighbors, keep only possible pieces
252+
candidates = neighbors[neigh_vals[0]]
253+
if len(neigh_offset) == 2:
254+
candidates = [c for c in candidates if c in neighbors[neigh_vals[1]]]
255+
256+
# Remove elements already in image
257+
cameras_in_image = list(map(lambda a: a[0], image_pieces.values()))
258+
candidates = [c for c in candidates if c[0] not in cameras_in_image]
259+
260+
# Final filter on the orientation
261+
candidates = [
262+
c for c in candidates if find_best_orientation([c], cam_pos, neigh_vals)
263+
]
264+
265+
assert len(candidates) == 1
266+
267+
image_pieces[cam_pos] = candidates[0]
268+
269+
# Merge all the pieces
270+
all_pieces = []
271+
for y in range(0, -grid_size, -1):
272+
for x in range(grid_size):
273+
base_image = cam_image[image_pieces[x + 1j * y][0]]
274+
orientation = image_pieces[x + 1j * y][1]
275+
new_piece = base_image.flip(orientation[1])[0].rotate(orientation[0])[0]
276+
new_piece = new_piece.crop([1 - 1j, cam_size - 2 - 1j * (cam_size - 2)])
277+
all_pieces.append(new_piece)
278+
279+
final_image = grid.merge_grids(all_pieces, grid_size, grid_size)
280+
del all_pieces
281+
del orientation
282+
del image_pieces
283+
284+
# Let's search for the monsters!
285+
monster = " # \n# ## ## ###\n # # # # # # "
286+
dash_in_monster = Counter(monster)["#"]
287+
monster = monster.replace(" ", ".").split("\n")
288+
monster_width = len(monster[0])
289+
line_width = (cam_size - 2) * grid_size
290+
291+
monster_found = defaultdict(int)
292+
for angle in all_angles:
293+
new_image = final_image.flip(angle[1])[0].rotate(angle[0])[0]
294+
text_image = new_image.dots_to_text()
295+
296+
matches = re.findall(monster[1], text_image)
297+
if matches:
298+
for match in matches:
299+
position = text_image.find(match)
300+
# We're on the first line
301+
if position <= line_width:
302+
continue
303+
if re.match(
304+
monster[0],
305+
text_image[
306+
position
307+
- (line_width + 1) : position
308+
- (line_width + 1)
309+
+ monster_width
310+
],
311+
):
312+
if re.match(
313+
monster[2],
314+
text_image[
315+
position
316+
+ (line_width + 1) : position
317+
+ (line_width + 1)
318+
+ monster_width
319+
],
320+
):
321+
monster_found[angle] += 1
322+
323+
if len(monster_found) != 1:
324+
# This means there was an error somewhere
325+
print(monster_found)
326+
327+
puzzle_actual_result = Counter(text_image)["#"] - dash_in_monster * max(
328+
monster_found.values()
329+
)
330+
331+
332+
# -------------------------------- Outputs / results --------------------------------- #
333+
334+
print("Case :", case_to_test, "- Part", part_to_test)
335+
print("Expected result : " + str(puzzle_expected_result))
336+
print("Actual result : " + str(puzzle_actual_result))
337+
# Date created: 2020-12-20 06:00:58.382556
338+
# Part 1: 2020-12-20 06:54:30
339+
# Part 2: 2020-12-20 16:45:45

0 commit comments

Comments
 (0)