Skip to content

Commit 4611d5d

Browse files
committed
Added a Pentominos Solver.
Works by brute force DFS.
1 parent a072fa7 commit 4611d5d

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

Diff for: puzzles/Pentominos Solver/Python/pentominos.py

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import os
2+
from colorama import init, deinit, Fore, Back, Style
3+
clear = lambda: os.system('cls')
4+
clear()
5+
6+
init()
7+
8+
colors = [Fore.BLACK, Fore.WHITE, Fore.WHITE + Style.BRIGHT, Fore.RED + Style.DIM, Style.BRIGHT + Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN, Fore.RED + Style.BRIGHT, Fore.BLUE + Style.BRIGHT, Fore.MAGENTA + Style.BRIGHT, Fore.YELLOW + Style.BRIGHT]
9+
10+
# Shapes
11+
shapes = [
12+
{ "variations":
13+
("OOOO\n" # L
14+
"O..."),
15+
"used": False },
16+
17+
{ "variations":
18+
("..OO\n" # N
19+
"OOO."),
20+
"used": False },
21+
22+
{ "variations":
23+
("OOO\n" # P
24+
".OO"),
25+
"used": False },
26+
27+
{ "variations":
28+
("OOO\n" # T
29+
".O.\n"
30+
".O."),
31+
"used": False },
32+
33+
{ "variations":
34+
("O.O\n" # U
35+
"OOO"),
36+
"used": False },
37+
38+
{ "variations":
39+
("O..\n" # V
40+
"O..\n"
41+
"OOO"),
42+
"used": False },
43+
44+
{ "variations":
45+
("..O\n" # W
46+
".OO\n"
47+
"OO."),
48+
"used": False },
49+
50+
{ "variations":
51+
(".O.\n" # X
52+
"OOO\n"
53+
".O."),
54+
"used": False },
55+
56+
{ "variations":
57+
("OOOO\n" # Y
58+
".O.."),
59+
"used": False },
60+
61+
{ "variations":
62+
("OO.\n" # Z
63+
".O.\n"
64+
".OO"),
65+
"used": False },
66+
67+
{ "variations":
68+
("OOOOO"),
69+
"used": False
70+
}, # I
71+
72+
{ "variations":
73+
(".OO\n" # F
74+
"OO.\n"
75+
".O."),
76+
"used": False }
77+
]
78+
79+
# Interpret shape strings above
80+
81+
for i in range(0, len(shapes)):
82+
variations = []
83+
84+
shape = shapes[i]["variations"].split("\n")
85+
86+
# Turn the list of strings of the shape into list of lists of 0's and 1's representing shape
87+
for j in range(0, len(shape)):
88+
shape[j] = list(shape[j])
89+
90+
shape[j] = [0 if c == "." else 1 for c in shape[j]]
91+
92+
mirror = [list(reversed(row)) for row in shape]
93+
94+
for _ in range(0, 4):
95+
shape = [list(elem) for elem in zip(*shape[::-1])] # Rotate clockwise
96+
mirror = [list(elem) for elem in zip(*mirror[::-1])]
97+
98+
if not shape in variations:
99+
variations.append(shape)
100+
101+
if not mirror in variations:
102+
variations.append(mirror)
103+
104+
shapes[i]["variations"] = variations
105+
106+
input_string = input("Enter width and height in format 'w h'. Only multiples of 5 work.: \n").split(" ")
107+
width = int(input_string[0])
108+
height = int(input_string[1])
109+
pieces_needed = (width * height) / 5
110+
board = [[0 for i in range(0, width)] for j in range(0, height)]
111+
num_steps = 0
112+
113+
def print_board():
114+
global num_steps
115+
116+
for r in board:
117+
s = ""
118+
for c in r:
119+
s += colors[c] + "██" + Style.RESET_ALL
120+
121+
print(s + Style.RESET_ALL + "|")
122+
123+
print("----------------------------")
124+
print(num_steps)
125+
126+
def try_place_shape(shape, x, y, id, adj):
127+
if y + len(shape) > height or x + len(shape[0]) > width:
128+
return False
129+
130+
foundAdj = False
131+
132+
for j, r in enumerate(shape):
133+
by = y + j
134+
135+
for i, c in enumerate(r):
136+
bx = x + i
137+
138+
if c == 1:
139+
if board[by][bx] > 0:
140+
return False
141+
elif adj and not foundAdj: # Some shapes may not touch another when placed in their spot. It's useless to place those
142+
if bx > 0 and board[by][bx - 1] > 0:
143+
foundAdj = True
144+
elif bx + 1 < width and board[by][bx + 1] > 0:
145+
foundAdj = True
146+
elif by > 0 and board[by - 1][bx] > 0:
147+
foundAdj = True
148+
elif by + 1 < height and board[by + 1][bx] > 0:
149+
foundAdj = True
150+
else:
151+
foundAdj = False
152+
153+
if adj and not foundAdj:
154+
return False
155+
156+
for j, r in enumerate(shape):
157+
by = y + j
158+
for i, c in enumerate(r):
159+
bx = x + i
160+
161+
if c == 1:
162+
board[by][bx] = id
163+
164+
# Check for impossible areas. Done in one scan by combining areas it sees above with area's it's created by traveling from left to right
165+
areas = {}
166+
combines = {}
167+
ai = -1
168+
169+
for j, r in enumerate(board):
170+
for i, c in enumerate(r):
171+
if c == 0:
172+
left = 1
173+
up = 1
174+
175+
if i > 0:
176+
left = board[j][i - 1]
177+
if j > 0:
178+
up = board[j - 1][i]
179+
elif i == 0 and j == 0:
180+
areas[ai] = 1
181+
board[j][i] = ai
182+
ai -= 1
183+
continue
184+
185+
if left < 0 and up < 0:
186+
if left == up:
187+
areas[left] += 1
188+
else:
189+
areas[left] += 1
190+
combines[left] = up
191+
192+
board[j][i] = left
193+
elif left < 0:
194+
areas[left] += 1
195+
board[j][i] = left
196+
elif up < 0:
197+
areas[up] += 1
198+
board[j][i] = up
199+
else:
200+
ai -= 1
201+
areas[ai] = 1
202+
board[j][i] = ai
203+
204+
# Reset our math
205+
for j, r in enumerate(board):
206+
for i, c in enumerate(r):
207+
if c < 0:
208+
board[j][i] = 0
209+
210+
# Combine all areas
211+
for key, value in combines.items():
212+
v = value # Sometimes chains of areas may exist
213+
while v in combines:
214+
v = combines[v]
215+
216+
areas[v] += areas[key]
217+
del areas[key]
218+
219+
# Check if any areas are "cutoff". This is a slight optimization that sometimes prevents the contuation of a search that has an obvious "hole" that can only be filled by
220+
# an already used piece
221+
if len(areas) > 1:
222+
remove_shape(shape, x, y)
223+
return False
224+
225+
# If the area left is not a multiple of 5, then it is unsolvable.
226+
for key, value in areas.items():
227+
if value < 5 or value % 5 != 0:
228+
remove_shape(shape, x, y)
229+
return False
230+
231+
return True
232+
233+
def remove_shape(shape, x, y):
234+
for j, r in enumerate(shape):
235+
for i, c in enumerate(r):
236+
if c == 1:
237+
board[y + j][x + i] = 0
238+
239+
solution_stack = []
240+
241+
# It's safe to use a recursive search because the max depth we can go is 12.
242+
def recursive_search():
243+
global num_steps
244+
245+
for i, shape in enumerate(shapes):
246+
if shape["used"]:
247+
continue
248+
249+
for j, variation in enumerate(shape["variations"]):
250+
for x in range(0, width):
251+
for y in range(0, height):
252+
if try_place_shape(variation, x, y, i + 1, len(solution_stack) != 0):
253+
shape["used"] = True
254+
solution_stack.append({ "shape": variation, "x": x, "y": y})
255+
num_steps += 1
256+
257+
print_board()
258+
clear()
259+
260+
if len(solution_stack) == pieces_needed:
261+
return True
262+
elif recursive_search():
263+
return True
264+
else:
265+
shape["used"] = False
266+
solution_stack.pop()
267+
remove_shape(variation, x, y)
268+
269+
recursive_search()
270+
print_board()
271+
272+
input()
273+
deinit()

Diff for: puzzles/Pentominos Solver/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ABOUT PENTOMINOS
2+
3+
Pentominos is a simple puzzle game where the goal is to fit several varying shapes of area 5 units into another shape. There are 12 unique "pentominos" that can be placed. The largest version of the game can fit 12 pentominos into a 6x10 box with a total of 2339 solutions.
4+
5+
https://en.wikipedia.org/wiki/Pentomino

0 commit comments

Comments
 (0)