diff --git a/four_letter_blocks/block_packer.py b/four_letter_blocks/block_packer.py index ec81085..7c757a5 100644 --- a/four_letter_blocks/block_packer.py +++ b/four_letter_blocks/block_packer.py @@ -54,6 +54,14 @@ def __init__(self, if 0 <= min_tries < tries: self.stop_tries = tries - min_tries self.is_tracing = False + + # True if slots should be filled in random order, otherwise False if + # slots should be filled from top to bottom. + self.are_slots_shuffled = False + + # True if self.state should be set, even with a partial filling + self.are_partials_saved = False + self.fewest_unused: int | None = None self.slot_coverage = self.state @@ -100,7 +108,7 @@ def calculate_max_shape_counts(self): """ # noinspection PyUnresolvedReferences block_count = (self.state == 0).sum() // 4 - block_count += 5*7 # Add flexibility to make packing easier. + block_count += 7 # Add flexibility to make packing easier. multiplier = {'O': 4, 'S': 2, 'Z': 2, 'I': 2} shape_names = Block.shape_rotation_names() return {shape: math.ceil(multiplier.get(shape[0], 1) * block_count / 28) @@ -159,7 +167,7 @@ def find_slots(self) -> dict[str, np.ndarray]: slot_coverage += shape_coverage slots[shape] = usable_slots self.slot_coverage = slot_coverage - if slot_coverage.all(): + if self.are_partials_saved or slot_coverage.all(): return slots # Some unfilled spaces weren't covered by any usable slots, return empty. @@ -234,24 +242,27 @@ def create_block(self, block_num): block = Block(*squares) return block - def fill(self, - shape_counts: typing.Counter[str], - are_slots_shuffled: bool = False, - are_partials_saved: bool = False) -> bool: + def fill(self, shape_counts: typing.Counter[str]) -> bool: """ Fill in the current state with the given shapes. Cycles through the available shapes in shape_counts, and tries them in different positions, looking for the fewest rows. Set the current state to a filled in copy, not changing the original. + Slots with least coverage are always filled first. If + self.are_slots_shuffled is True, then coverage ties are broken randomly, + otherwise ties are filled from top to bottom. + + If self.are_partials_saved is True, then we don't cycle through options, + just make the first choice for each slot and return with self.state set. + :param shape_counts: number of blocks of each shape, disables rotation - if any of the shapes contain a letter and rotation number - :param are_slots_shuffled: True if slots should be filled in random - order, otherwise False if slots should be filled from top to bottom. - :param are_partials_saved: True if self.state should be set, even with - a partial filling + if any of the shapes contain a letter and rotation number. Adjusted + to remaining counts, if self.are_partials_saved is True. :return: True, if successful, otherwise False. """ + are_slots_shuffled = self.are_slots_shuffled + are_partials_saved = self.are_partials_saved if self.tries == 0: self.state = None return False @@ -260,6 +271,9 @@ def fill(self, best_state = None assert self.state is not None start_state = self.state + if not sum(shape_counts.values()): + # Nothing to add! + best_state = start_state next_block = self.find_next_block() fewest_rows = start_state.shape[0]+1 @@ -284,7 +298,6 @@ def fill(self, continue # noinspection PyTypeChecker shape_scores[shape] = -slot_count / target_count - is_filled = False for shape, _score in shape_scores.most_common(): if not is_rotation_allowed: slot_shapes = [shape] @@ -326,9 +339,7 @@ def fill(self, f'finished? {is_finished}') print(self.display()) if not is_finished and self.tries != 0: - is_filled = self.fill(shape_counts, - are_slots_shuffled, - are_partials_saved) + is_filled = self.fill(shape_counts) if not is_filled: continue used_rows = self.count_filled_rows() @@ -352,11 +363,9 @@ def fill(self, if are_partials_saved: break shape_counts[shape] = old_count - if is_filled: - return True - if ((not is_rotation_allowed or best_state is None) and - not are_partials_saved): - return False + # if ((not is_rotation_allowed or best_state is None) and + # not are_partials_saved): + # return False # new_state = start_state.copy() # # noinspection PyUnboundLocalVariable # new_state[target_row, target_col] = 1 # gap @@ -372,8 +381,8 @@ def fill(self, self.state = None return False - def find_next_block(self): - used_blocks = np.unique(self.state) + def find_next_block(self) -> int: + used_blocks = np.unique(self.state) # type: ignore block: int for i, block in enumerate(used_blocks[:-1]): if block >= self.GAP and used_blocks[i + 1] != block + 1: @@ -438,9 +447,9 @@ def count_filled_rows(self): def random_fill(self, shape_counts: typing.Counter[str]): """ Randomly place pieces from shape_counts on empty spaces. """ - self.fill(shape_counts, - are_slots_shuffled=True, - are_partials_saved=False) + self.are_slots_shuffled = True + self.are_partials_saved = False + self.fill(shape_counts) def flip(self) -> 'BlockPacker': assert self.state is not None diff --git a/four_letter_blocks/double_block_packer.py b/four_letter_blocks/double_block_packer.py index 4df471a..b66cc05 100644 --- a/four_letter_blocks/double_block_packer.py +++ b/four_letter_blocks/double_block_packer.py @@ -97,7 +97,9 @@ def fill(self) -> bool: shape_scores[shape] = -slot_count / target_count start_state1 = packer1.state + assert start_state1 is not None start_state2 = packer2.state + assert start_state2 is not None next_block = packer1.find_next_block() for shape1, _score in shape_scores.most_common(): shape2 = flipped_shape_names[shape1] diff --git a/four_letter_blocks/evo_packer.py b/four_letter_blocks/evo_packer.py index 760e82f..8f748e0 100644 --- a/four_letter_blocks/evo_packer.py +++ b/four_letter_blocks/evo_packer.py @@ -102,7 +102,11 @@ def pair(self, other, pair_params): mover2.move(i2, j2) packer = BlockPacker(start_state=new_state) packer.force_fours = True - packer.random_fill(mover1.shape_counts) + packer.are_slots_shuffled = True + packer.are_partials_saved = True + packer.fill(mover1.shape_counts) + + assert packer.state is not None return Packing(dict(state=packer.state, shape_counts=mover1.shape_counts, can_rotate=can_rotate, @@ -117,6 +121,8 @@ def mutate(self, mutate_params) -> None: can_rotate: bool = self.value['can_rotate'] block_packer = BlockPacker(start_state=state) block_packer.force_fours = True + block_packer.are_partials_saved = True + block_packer.are_slots_shuffled = True grid_size = state.shape[0] gaps = np.argwhere(state == 0) if gaps.size > 0: @@ -149,8 +155,9 @@ def mutate(self, mutate_params) -> None: if remove_count == 0: break - block_packer.random_fill(shape_counts) + block_packer.fill(shape_counts) + assert block_packer.state is not None self.value = dict(state=block_packer.state, shape_counts=shape_counts, can_rotate=can_rotate) @@ -161,7 +168,11 @@ def _random_init(self, init_params: dict): can_rotate = all(len(shape) == 1 for shape in shape_counts) block_packer = BlockPacker(start_state=start_state) block_packer.force_fours = True - block_packer.random_fill(shape_counts) + block_packer.are_slots_shuffled = True + block_packer.are_partials_saved = True + block_packer.fill(shape_counts) + + assert block_packer.state is not None return dict(state=block_packer.state, shape_counts=shape_counts, can_rotate=can_rotate) @@ -270,6 +281,7 @@ def __init__(self, start_state) self.is_logging = False self.epochs = 100 + self.pool_size = 1000 self.current_epoch = 0 self.shape_counts: typing.Counter[str] = Counter() self.evo: Evolution | None = None @@ -287,10 +299,10 @@ def setup(self, fitness_calculator.summaries.clear() self.evo = Evolution( - pool_size=1000, + pool_size=self.pool_size, fitness=fitness_calculator.calculate, individual_class=Packing, - n_offsprings=200, + n_offsprings=self.pool_size // 5, pair_params=None, mutate_params=None, init_params=init_params, diff --git a/four_letter_blocks/puzzle_set.py b/four_letter_blocks/puzzle_set.py index f1427bd..0727ef0 100644 --- a/four_letter_blocks/puzzle_set.py +++ b/four_letter_blocks/puzzle_set.py @@ -26,7 +26,8 @@ def __init__(self, self.block_packer = block_packer else: self.block_packer = BlockPacker(16, 20, # Game Crafter cutout size - tries=10_000) + tries=10_000, + min_tries=1) self.front_blocks: typing.Dict[ str, typing.List[Block | None]] = defaultdict(list) diff --git a/tests/test_block_packer.py b/tests/test_block_packer.py index 2511e45..3f38e26 100644 --- a/tests/test_block_packer.py +++ b/tests/test_block_packer.py @@ -121,8 +121,8 @@ def test_fill_two_blocks(): width, height = 5, 4 shape_counts = Counter('LO') expected_display = dedent("""\ - ..ABB - AAABB + AA..B + AABBB ..... .....""") packer = BlockPacker(width, height) @@ -135,8 +135,8 @@ def test_fill_two_blocks_force_fours(): width, height = 5, 4 shape_counts = Counter('LO') expected_display = dedent("""\ - AAABB - A..BB + AABBB + AAB.. ..... .....""") packer = BlockPacker(width, height) @@ -196,7 +196,7 @@ def test_no_rotations_needs_gap(): width = height = 5 shape_counts = Counter(('O', 'J3', 'O')) expected_display = dedent("""\ - AA#CC + AA.CC AABCC ..BBB ..... @@ -217,7 +217,9 @@ def test_random_fill(): ..... ..##.""") packer = BlockPacker(start_text=start_text) - packer.random_fill(shape_counts) + packer.are_slots_shuffled = True + packer.are_partials_saved = True + packer.fill(shape_counts) assert 1 <= shape_counts['O'] <= 3 @@ -231,8 +233,10 @@ def test_random_fill_lower_numbers(): ..#.. ..DDD ..##D""") - packer = BlockPacker(start_text=start_text) - packer.random_fill(shape_counts) + packer = BlockPacker(start_text=start_text, tries=100, min_tries=1) + packer.are_slots_shuffled = True + packer.are_partials_saved = True + packer.fill(shape_counts) assert 2 <= shape_counts['O'] <= 3 assert packer.state.max() == 5 @@ -245,7 +249,9 @@ def test_random_fill_tries_multiple_shapes(): AA...B AA.BBB""") packer = BlockPacker(start_text=start_text) - packer.random_fill(shape_counts) + packer.are_slots_shuffled = True + packer.are_partials_saved = True + packer.fill(shape_counts) assert shape_counts['L'] == 0 @@ -254,7 +260,9 @@ def test_random_fill_no_gaps(): for _ in range(100): shape_counts = Counter({'O': 1}) packer = BlockPacker(2, 2) - packer.random_fill(shape_counts) + packer.are_slots_shuffled = True + packer.are_partials_saved = True + packer.fill(shape_counts) assert shape_counts['O'] == 0 assert packer.state.max() == 2 @@ -356,10 +364,10 @@ def test_fill_with_split_row(): AA. AA. ... - BBB - .B. - CC. - CC.""") + BB. + BB. + CCC + .C.""") packer = BlockPacker(width, height, split_row=3, tries=100) packer.fill(shape_counts) @@ -375,7 +383,7 @@ def test_fill_overflow(): assert len(packer.positions['O']) == 254 - shape_counts['O'] += 1 + shape_counts = Counter({'O': 255}) packer = BlockPacker(256, 4, tries=500) with pytest.raises(ValueError, match='Maximum 254 blocks in packer.'): packer.fill(shape_counts) @@ -490,8 +498,9 @@ def test_shape_counts_7x7(): ....... ...#... #.....#""")) - expected_shape_counts = {name: 1 for name in Block.shape_names()} - expected_shape_counts['O'] = 2 + expected_shape_counts = { + name: 3 if name == 'O' else 2 if name[0] in 'ISZ' else 1 + for name in Block.shape_rotation_names()} shape_counts = packer.calculate_max_shape_counts() @@ -509,11 +518,9 @@ def test_shape_counts_9x9(): ......... ...#..... ...#.....""")) - expected_shape_counts = {name: 1 for name in Block.shape_names()} - expected_shape_counts['O'] = 3 - for name in expected_shape_counts: - if name[0] in 'ISZ': - expected_shape_counts[name] = 2 + expected_shape_counts = { + name: 4 if name == 'O' else 2 if name[0] in 'ISZ' else 1 + for name in Block.shape_rotation_names()} shape_counts = packer.calculate_max_shape_counts() diff --git a/tests/test_evo_packer.py b/tests/test_evo_packer.py index 190c33d..7727cac 100644 --- a/tests/test_evo_packer.py +++ b/tests/test_evo_packer.py @@ -23,8 +23,10 @@ def test_no_rotations(): DA#EC DEEEC DD##C""") - packer = EvoPacker(start_text=start_text) + packer = EvoPacker(start_text=start_text, tries=100, min_tries=1) packer.is_logging = False + packer.epochs = 2 + packer.pool_size = 100 is_filled = packer.fill(shape_counts) packer.sort_blocks() diff --git a/tests/test_puzzle_pair.py b/tests/test_puzzle_pair.py index fc48ab6..419f8c0 100644 --- a/tests/test_puzzle_pair.py +++ b/tests/test_puzzle_pair.py @@ -497,9 +497,8 @@ def test_pack_greetings(): min_tries=1) back_shapes = packer.calculate_max_shape_counts() packer.force_fours = True - packer.fill(back_shapes, - are_slots_shuffled=True, - are_partials_saved=False) + packer.are_slots_shuffled = True + packer.fill(back_shapes) if not packer.is_full: print('First packer failed.') continue @@ -515,9 +514,8 @@ def test_pack_greetings(): tries=1000, min_tries=1) packer2.force_fours = True - packer2.fill(target_shapes, - are_partials_saved=False, - are_slots_shuffled=True) + packer2.are_slots_shuffled = True + packer2.fill(target_shapes) packer.sort_blocks() packer2.sort_blocks() display1 = packer.display() diff --git a/tests/test_puzzle_set.py b/tests/test_puzzle_set.py index dede4c6..c44c293 100644 --- a/tests/test_puzzle_set.py +++ b/tests/test_puzzle_set.py @@ -304,18 +304,18 @@ def test_draw_packed(pixmap_differ: PixmapDiffer): puzzle_set1.square_size = 20 blocks = puzzle1.blocks - blocks[0].set_display(270, 10, 0) - blocks[1].set_display(170, 30, 0) - blocks[2].set_display(50, 10, 0) - blocks[3].set_display(10, 70, 0) - blocks[4].set_display(30, 70, 0) + blocks[0].set_display(250, 10, 0) + blocks[1].set_display(230, 70, 0) + blocks[2].set_display(110, 30, 0) + blocks[3].set_display(10, 10, 0) + blocks[4].set_display(70, 10, 2) blocks = puzzle2.blocks - blocks[0].set_display(70, 50, 0) - blocks[1].set_display(70, 90, 1) + blocks[0].set_display(90, 90, 0) + blocks[1].set_display(10, 50, 0) blocks[2].set_display(170, 10, 0) - blocks[3].set_display(250, 70, 0) - blocks[4].set_display(230, 10, 0) + blocks[3].set_display(210, 10, 2) + blocks[4].set_display(170, 30, 0) for block in puzzle1.blocks + puzzle2.blocks: block.draw(expected, is_packed=True) @@ -334,11 +334,12 @@ def test_draw_cuts(pixmap_differ: PixmapDiffer): 180, 'test_puzzle_draw_cuts') as (actual, expected): block_text = dedent("""\ - #ABCCCC - #ABBBDD - AAEEDDE - F#EEGGE - FFFGGEE + AABCCDD + AABCDDE + FBBCGGE + F##HGEE + F##HGII + F#HHII# """) puzzle3 = Puzzle.parse_sections('', block_text,