import numpy as np
import heapq as hq
from collections import defaultdict

class Rectangle:
    def __init__(self, h, w, x, y, n, m):
        self.h, self.w = h, w
        self.x, self.y = x, y
        self.n, self.m = n, m
        self.mask = self.bit_mask()
        self.area = sum(self.mask)
    
    def bit_mask(self):
        mask = np.zeros((self.n, self.m), dtype=bool)
        mask[self.x : self.x + self.h, self.y : self.y + self.w] = True
        return mask.flatten()
    
    def __repr__(self):
        return f"Rectangle at ({self.x}, {self.y}) of size h = {self.h} and w = {self.w}\nMask : {self.mask}"

class Board:
    def __init__(self, rows, rectangles):
        self.n, self.m = len(rows), len(rows[0])
        mask = np.zeros((self.n, self.m), dtype=bool)
        self.frees = set()
        for i in range(self.n):
            for j in range(self.m):
                if rows[i][j] == " ":
                    mask[i][j] = True
        self.mask = mask.flatten()

        print(self.mask)
        self.frees = set([i for i, x in enumerate(self.mask) if not x])
        self.positions = self.get_rectangle_positions(rectangles)
    
    def show(self, mask):
        return "\n".join(
            "".join("." if b else "O" for b in mask[i * self.m : (i + 1) * self.m])
            for i in range(self.n)
        )
    
    def __repr__(self):
        pos_str = "\n".join(f"{k}: {v}" for k, v in self.positions.items())
        return f"Board\nMask :\n{self.show(self.mask)}\nFree positions :{self.frees}"
    
    def get_rectangle_positions(self, rects):
        pos = defaultdict(list)
        for free in self.frees:
            x, y = divmod(free, self.m)
            for idx, rect in enumerate(rects):
                h, w = rect 
                r = Rectangle(h, w, x, y, self.n, self.m)
                mask = r.mask
                area = r.area
                if not ((mask & self.mask).any()):
                    pos[free].append((idx, mask, area, 0))
                if h != w:
                    h, w = w, h
                    r = Rectangle(h, w, x, y, self.n, self.m)
                    mask = r.mask
                    area = r.area
                    if not ((mask & self.mask).any()):
                        pos[free].append((idx, mask, area, 1))
        return pos
    
    def solve(self):
        q = []
        hq.heapify(q)
        counter = 0  # Add a counter for tie-breaking
        
        for free in self.frees:
            for idx, mask, area, rot in self.positions[free]:
                if not ((mask & self.mask).any()):
                    new_mask = self.mask | mask  
                    # Add counter as tie-breaker to avoid comparing numpy arrays
                    ele = (-area, counter, new_mask, [(idx, free, rot)], set([idx]))
                    hq.heappush(q, ele)
                    counter += 1
        
        while len(q) > 0:
            _, _, mask, path, used = hq.heappop(q)  # Added extra _ for counter
            # print("used:", used)
            # print("path:", path)
            # print(self.show(mask))
            if mask.all():
                return path
            
            # Find first free position
            fst = None
            for i, val in enumerate(mask):
                if not val:
                    fst = i
                    break
            
            if fst is None:
                continue
                
            for idx, _mask, area, rot in self.positions[fst]:
                if idx not in used and not (_mask & mask).any():
                    new_path = path + [(idx, fst, rot)]
                    new_used = used | set([idx])
                    new_mask = mask | _mask
                    counter += 1
                    hq.heappush(
                        q,
                        (
                            -area,
                            counter,  # Add counter for tie-breaking
                            new_mask,
                            new_path,
                            new_used,
                        ),
                    )
        
        return None  # No solution found

board = [
    "            ",
    " 00000      ",
    " 00000      ",
    " 00000   00 ",
    "       000  ",
    "   00  000  ",
    " 0000 00    ",
    " 0000 00    ",
    " 00   000 0 ",
    "      000 0 ",
    "  0       0 ",
    "000         ",
]
pieces = [
    [1, 1],
    [1, 1],
    [1, 2],
    [1, 2],
    [1, 2],
    [1, 3],
    [1, 3],
    [1, 4],
    [1, 4],
    [2, 2],
    [2, 2],
    [2, 3],
    [2, 3],
    [2, 5],
]
# [(idx, free, rot)],
board = [
    "  000", 
    "00000"
        ]
'''
    "  200", 
    "31100"
    '''
pieces  = [[2,2], [2, 1], [1, 1], [1, 1]]
b = Board(board, pieces)
print(b)
r = b.solve()
print("r :", r)

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: