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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy