Skip to content

Commit

Permalink
Find slots for each shape rotation before filling.
Browse files Browse the repository at this point in the history
Part of #56.
  • Loading branch information
donkirkby committed Feb 25, 2024
1 parent 311da2e commit 557c990
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 160 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ name = "pypi"
PySide6 = "<6.5" # 6.5.0 causes fatal Python error. Try 6.5.1 when released.
four-letter-blocks = {editable = true, path = "."}
beautifulsoup4 = "*"
scipy = "*"

[dev-packages]
four-letter-blocks = {editable = true, extras = ["dev"], path = "."}
Expand Down
369 changes: 213 additions & 156 deletions Pipfile.lock

Large diffs are not rendered by default.

47 changes: 45 additions & 2 deletions four_letter_blocks/block_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from random import shuffle

import numpy as np
from scipy.ndimage import label # type: ignore

from four_letter_blocks.block import shape_rotations, normalize_coordinates, Block
from four_letter_blocks.square import Square
Expand All @@ -21,6 +22,18 @@ def __init__(self,
start_text: str | None = None,
start_state: np.ndarray | None = None,
split_row=0):
""" Initialize a BlockPacker instance.
:param width: number of letters across the packed grid
:param height: number of letters down the packed grid
:param tries: maximum number of times to try finding a packing before
giving up. -1 means never give up.
:param min_tries: minimum number of times to try finding a packing
before accepting the best so far. -1 means use maximum tries.
:param start_text: grid with partial packing filled in
:param start_state: numpy array with partial packing filled in
:param split_row: row number that can evenly split the grid into two
pages. split_row is the bottom row of the first page.
"""
self.state: np.ndarray | None
if start_state is not None:
self.height, self.width = start_state.shape
Expand All @@ -44,6 +57,7 @@ def __init__(self,
self.stop_tries = 0
if 0 <= min_tries < tries:
self.stop_tries = tries - min_tries
self.slots: dict[str, list[int]] = {}

@property
def positions(self):
Expand Down Expand Up @@ -87,6 +101,31 @@ def display(self, state: np.ndarray | None = None) -> str:
for c in row)
for row in state)

def find_slots(self) -> dict[str, list[int]]:
for squares, (shape, rotation) in shape_rotations().items():
shape_slots = []
if shape == 'O':
name = shape
else:
name = f'{shape}{rotation}'
for i in range(self.height):
for j in range(self.width):
for new_state in self.place_block(name, i, j, 2):
gaps = new_state == self.UNUSED
grouped, group_count = label(gaps)
bin_counts = np.bincount(grouped.flatten())
unused_group_sizes = bin_counts[1:]
has_uneven_group = False
for group_size in unused_group_sizes:
if group_size % 4 != 0:
has_uneven_group = True
break
if not has_uneven_group:
slot = i*self.width + j
shape_slots.append(slot)
self.slots[name] = shape_slots
return self.slots

def sort_blocks(self):
state = np.zeros(self.state.shape, np.uint8)
gap_spaces = self.state == 1
Expand All @@ -102,6 +141,7 @@ def sort_blocks(self):
if old_block <= 1:
# gap or space
continue
# noinspection PyUnresolvedReferences
block_spaces = (self.state == old_block).astype(np.uint8)
state += next_block * block_spaces
next_block += 1
Expand Down Expand Up @@ -165,8 +205,10 @@ def fill(self, shape_counts: typing.Counter[str]) -> bool:
# No empty spaces left, fail.
self.state = None
return False
target_row = empty[0][0]
target_col = empty[1][0]
# noinspection PyTypeChecker
target_row: int = empty[0][0]
# noinspection PyTypeChecker
target_col: int = empty[1][0]
next_block = np.amax(start_state) + 1
if next_block == self.GAP:
next_block += 1
Expand Down Expand Up @@ -278,6 +320,7 @@ def random_fill(self, shape_counts: typing.Counter[str]):
empty = np.argwhere(self.state == 0)
np.random.shuffle(empty)
used_blocks = np.unique(self.state)
block: int
for i, block in enumerate(used_blocks[:-1]):
if block >= self.GAP and used_blocks[i+1] != block+1:
next_block = block + 1
Expand Down
4 changes: 3 additions & 1 deletion four_letter_blocks/puzzle_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from four_letter_blocks.block import Block
from four_letter_blocks.block_packer import BlockPacker
from four_letter_blocks.puzzle import Puzzle, draw_rotated_tiles
from four_letter_blocks.puzzle import Puzzle, draw_rotated_tiles, RotationsDisplay


class PuzzleSet:
Expand Down Expand Up @@ -51,6 +51,8 @@ def __init__(self,
self.pack_puzzles()

def pack_puzzles(self):
for i, puzzle in enumerate(self.puzzles):
puzzle.rotations_display = RotationsDisplay.FRONT
combos = self.combos
pairs = self.pairs
total_counts = Counter()
Expand Down
16 changes: 16 additions & 0 deletions tests/test_block_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,19 @@ def test_fill_fail():
is_filled = packer.fill(shape_counts)

assert not is_filled


def test_find_slots():
packer = BlockPacker(start_text=dedent("""\
#..#.#####
.....#####
..#..#####
.....#####
.#..######"""))
# Not at 13 or 20, because they cut off something.
expected_o_slots = [1, 10, 23, 32]

o_slots = packer.find_slots()['O']

assert o_slots == expected_o_slots

3 changes: 2 additions & 1 deletion tests/test_puzzle_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from four_letter_blocks.block import Block
from four_letter_blocks.block_packer import BlockPacker
from four_letter_blocks.evo_packer import EvoPacker
from four_letter_blocks.puzzle import Puzzle
from four_letter_blocks.puzzle import Puzzle, RotationsDisplay
from four_letter_blocks.puzzle_set import PuzzleSet
from four_letter_blocks import four_letter_blocks_rc
from tests.pixmap_differ import PixmapDiffer
Expand Down Expand Up @@ -67,6 +67,7 @@ def test_summary():
puzzle_set = parse_puzzle_set()
puzzle1, puzzle2 = puzzle_set.puzzles

assert puzzle1.rotations_display == RotationsDisplay.FRONT
summary1 = puzzle1.display_block_summary()
assert summary1 == 'Block sizes: 5x4, Shapes: J: 2, L: 2, O: 1'
summary2 = puzzle2.display_block_summary()
Expand Down

0 comments on commit 557c990

Please sign in to comment.