I'm working on a puzzle game and I need all the free polyominoes that can fit into an 8 by 8 grid. My code uses Redelmeier's Alogorithm for enumerating them, checks them for rotations and reflections, and makes sure it doesn't go outside the 8 by 8 grid. This isn't a packing problem. Each one is checked one at a time against this criteria.
Polyominoes are categorized by their area (i.e. the number of cells they contain). For example, you're probably familiar with Tetris. All of the tetris pieces have an area of 4. The number of pieces grows exponentially with the area.
Free polyominoes consider rotations, translations, and reflections identical. So the domino with two cells side by side is considered the same as the one with the cells arranged vertically. This is in contrast to fixed polyominoes where rotations, translations, and reflections are considered different. For my purposes, rotating a puzzle doesn't change it so it's duplicate data I don't want to save.
I mentioned the number of pieces grows exponentially and when the area is 64, the number of fixed polyominoes is 4,496,670,726,609,716,846,990,603,851,802,851,046. Burnside's Lemma will reduce this by about 8. That's still a lot but that set also contains a lot of aren't within the bounds of my constraints. This would include a lot that are mostly tall and thin or too wide and narrow. There's no way the upper bound could exceed the number of ways to arrange black and white pixels in an 8x8 image which would be 2^64 or 18,446,744,073,709,551,616.
I believe my program is correct because it produces free polyominoes from oeis A000105 up until n = 9. When n = 9, there's a single piece from that set that is too wide (the 1 by 9) and it gets removed. So I'm fairly confident that it works. The only problem is that it's slow and will take several 100 years to finish. I'm not sure what I can do to speed this up.
Lastly, it's saving the output as list of bitboards to a text file which I've commented out for you. I have another program that reads them.
import itertools
from collections import defaultdict
from timeit import default_timer as timer
# I'm only going to n = 63 because n = 64 is trivial.
n = 63
print("Number of Polyomino pieces up to size", n)
temp_output = []
smallest_piece = [(0, 0)]
pieces_of_size = {1: [smallest_piece]}
def possible_expansions(piece):
neighbor_offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]
positions = set()
for (x, y) in piece:
for dx, dy in neighbor_offsets:
positions.add((x + dx, y + dy))
expansions = []
piece_set = set(piece)
for p in positions:
if p not in piece_set:
expansions.append(piece + [p])
return expansions
def rotate_90_cw(piece):
return [(y, -x) for (x, y) in piece]
def flip(piece):
return [(-x, y) for (x, y) in piece]
def canonical(piece):
min_x = min(x for (x, y) in piece)
min_y = min(y for (x, y) in piece)
res = sorted((x - min_x, y - min_y) for (x, y) in piece)
return res
def hash_piece(piece):
return hash(tuple(piece))
def expand_pieces(pieces):
expanded = []
temp_output = []
expanded_hashes = defaultdict(list)
for piece in pieces:
for e in possible_expansions(piece):
exp = canonical(e)
if (max(x for (x, y) in exp) >= 8) or (max(y for (x, y) in exp) >= 8):
continue
is_new = True
if exp in expanded_hashes[hash_piece(exp)]:
is_new = False
continue
for rotation in range(3):
exp = canonical(rotate_90_cw(exp))
if exp in expanded_hashes[hash_piece(exp)]:
is_new = False
continue
exp = canonical(flip(exp))
if exp in expanded_hashes[hash_piece(exp)]:
is_new = False
continue
for rotation in range(3):
exp = canonical(rotate_90_cw(exp))
if exp in expanded_hashes[hash_piece(exp)]:
is_new = False
continue
if is_new:
temp_output.append(str(coordinates_list_to_bitboard(exp)) + "\n")
expanded.append(exp)
expanded_hashes[hash_piece(exp)].append(exp)
# I've commented this out so it doesn't save anything to your hard drive.
#output = "".join(temp_output)
#f = open("C:\\Users\\rober\\Documents\\" + "size is " + str(len(exp)) +".txt", "w")
#f.write(output)
return expanded
def coordinates_list_to_bitboard(coordinates_list):
bitboard = 0
for x, y in coordinates_list:
if 0 <= x < 8 and 0 <= y < 8:
bit_position = y * 8 + x
bitboard |= 1 << bit_position
else:
print(coordinates_list)
raise ValueError(f"Coordinates ({x}, {y}) are out of bounds for an 8x8 board")
return bitboard
for i in range(2, n + 1):
start = timer()
pieces_of_size[i] = expand_pieces(pieces_of_size[i - 1])
print("Pieces with {} blocks: {}".format(i, len(pieces_of_size[i])))
end = timer()
print(end - start)
I think something about running it in Visual Studio Community is really slowing it down. I'm about 35x slower right out the gate compared with others.