diff --git a/four_letter_blocks/block_packer.py b/four_letter_blocks/block_packer.py index a430825..a09c8e0 100644 --- a/four_letter_blocks/block_packer.py +++ b/four_letter_blocks/block_packer.py @@ -1,7 +1,7 @@ import typing -from collections import defaultdict +from collections import defaultdict, Counter from functools import cache -from random import shuffle +from random import randrange import numpy as np from scipy.ndimage import label # type: ignore @@ -41,6 +41,7 @@ def __init__(self, else 1 if char == '#' else ord(char) - 63) self.split_row = split_row + self.force_fours = False self.tries = tries self.stop_tries = 0 if 0 <= min_tries < tries: @@ -79,27 +80,39 @@ def find_slots(self, is_rotation_allowed=False) -> dict[str, np.ndarray]: raise RuntimeError('Cannot find slots with invalid state.') all_masks = build_masks(self.width, self.height) + shape_heights = get_shape_heights() slots = {} padded = np.pad(self.state.astype(bool), (0, 3), constant_values=1) for shape, masks in all_masks.items(): collisions = np.logical_and(masks, padded) colliding_positions = np.any(collisions, axis=(2, 3)) - open_slots = np.logical_not(colliding_positions) - - gaps = np.logical_not(np.logical_or(masks, padded)) - structure = np.zeros((3, 3, 3, 3), bool) - structure[1, 1, :, :] = [[0, 1, 0], - [1, 1, 1], - [0, 1, 0]] - gap_groups, group_count = label(gaps, structure=structure) - bin_counts = np.bincount(gap_groups.flatten()) - uneven_groups, = np.nonzero(bin_counts % 4) - if uneven_groups[0] == 0: - uneven_groups = uneven_groups[1:] - is_uneven = np.isin(gap_groups, uneven_groups) - has_even = np.logical_not(np.any(is_uneven, axis=(2, 3))) - - usable_slots = np.logical_and(open_slots, has_even) + + # Check for rows that cross the split row. + shape_height = shape_heights[shape] + crossing_positions = np.zeros_like(colliding_positions) + crossing_positions[ + self.split_row-shape_height+1:self.split_row, :] = True + + open_slots = np.logical_not(np.logical_or(colliding_positions, + crossing_positions)) + + if not self.force_fours: + usable_slots = open_slots + else: + gaps = np.logical_not(np.logical_or(masks, padded)) + structure = np.zeros((3, 3, 3, 3), bool) + structure[1, 1, :, :] = [[0, 1, 0], + [1, 1, 1], + [0, 1, 0]] + gap_groups, group_count = label(gaps, structure=structure) + bin_counts = np.bincount(gap_groups.flatten()) + uneven_groups, = np.nonzero(bin_counts % 4) + if uneven_groups[0] == 0: + uneven_groups = uneven_groups[1:] + is_uneven = np.isin(gap_groups, uneven_groups) + has_even = np.logical_not(np.any(is_uneven, axis=(2, 3))) + + usable_slots = np.logical_and(open_slots, has_even) if not is_rotation_allowed or len(shape) == 1: slots[shape] = usable_slots else: @@ -179,7 +192,10 @@ def create_block(self, block_num): block = Block(*squares) return block - def fill(self, shape_counts: typing.Counter[str]) -> bool: + def fill(self, + shape_counts: typing.Counter[str], + are_slots_shuffled: bool = False, + are_partials_saved: bool = False) -> bool: """ Fill in the current state with the given shapes. Cycles through the available shapes in shape_counts, and tries them in @@ -188,6 +204,10 @@ def fill(self, shape_counts: typing.Counter[str]) -> bool: :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 :return: True, if successful, otherwise False. """ if self.tries == 0: @@ -198,41 +218,62 @@ def fill(self, shape_counts: typing.Counter[str]) -> bool: best_state = None assert self.state is not None start_state = self.state - empty = np.nonzero(self.state == 0) - if len(empty[0]) == 0: - # No empty spaces left, fail. - self.state = None - return False - # 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 - elif next_block > 255: - raise ValueError('Maximum 254 blocks in packer.') - + 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 + break + else: + next_block = used_blocks[-1] + 1 + if next_block == self.GAP: + next_block += 1 + elif next_block > 255: + raise ValueError('Maximum 254 blocks in packer.') has_shapes = False - is_rotation_allowed = True fewest_rows = start_state.shape[0]+1 - for shape_name, _ in shape_counts.most_common(): - old_count = shape_counts[shape_name] - if old_count == 0: + + is_rotation_allowed = all(len(shape) == 1 for shape in shape_counts) + slots = self.find_slots(is_rotation_allowed) + shape_scores: typing.Counter[str] = Counter() + for shape, shape_slots in slots.items(): + target_count = shape_counts[shape] + if target_count == 0: + continue + slot_count = slots[shape].sum() + if slot_count == 0 and are_partials_saved: + # Don't try shape with no slots, but don't give up, either. continue + shape_scores[shape] = -slot_count / target_count + for shape, _score in shape_scores.most_common(): + slot_rows, slot_cols = np.nonzero(slots[shape]) + if len(slot_rows) == 0: + # No empty spaces left, fail. + if not are_partials_saved: + self.state = None + return False + if are_slots_shuffled: + slot_index = randrange(len(slot_rows)) + else: + slot_index = 0 + # noinspection PyTypeChecker + target_row: int = slot_rows[slot_index] + # noinspection PyTypeChecker + target_col: int = slot_cols[slot_index] + + old_count = shape_counts[shape] has_shapes = True - shape_counts[shape_name] = old_count - 1 - if len(shape_name) > 1: - is_rotation_allowed = False + shape_counts[shape] = old_count - 1 self.state = start_state - for new_state in self.place_block(shape_name, + for new_state in self.place_block(shape, target_row, target_col, next_block): self.state = new_state if sum(shape_counts.values()): - self.fill(shape_counts) - if self.state is None: + if not self.fill(shape_counts, + are_slots_shuffled, + are_partials_saved): continue used_rows = self.count_filled_rows() if used_rows < fewest_rows: @@ -240,23 +281,30 @@ def fill(self, shape_counts: typing.Counter[str]) -> bool: fewest_rows = used_rows if 0 <= self.tries <= self.stop_tries and best_state is not None: break - shape_counts[shape_name] = old_count + if are_partials_saved: + break if 0 <= self.tries <= self.stop_tries and best_state is not None: break + if are_partials_saved: + break + shape_counts[shape] = old_count if not has_shapes: return True - if not is_rotation_allowed or best_state is None: + if ((not is_rotation_allowed or best_state is None) and + not are_partials_saved): new_state = start_state.copy() + # noinspection PyUnboundLocalVariable new_state[target_row, target_col] = 1 # gap self.state = new_state - if self.fill(shape_counts): + if self.fill(shape_counts, are_slots_shuffled, are_partials_saved): used_rows = self.count_filled_rows() if used_rows < fewest_rows: best_state = self.state if best_state is not None: self.state = best_state return True - self.state = None + if not are_partials_saved: + self.state = None return False def place_block(self, @@ -270,7 +318,7 @@ def place_block(self, try all possible rotations. If it's a letter and number, only use the rotation given by the number :param target_row: row to try placing the block at - :param target_col: column to try placing the block at + :param target_col: column to try placing the block at (top-left) :param block_num: block value to place in the state :return: an iterator of states for each successful placement """ @@ -284,13 +332,9 @@ def place_block(self, rotation = int(shape_name[1]) allowed_blocks = blocks[shape_name[0]][rotation:rotation + 1] for block in allowed_blocks: - first_square_index = np.where(block[0])[0][0] new_state = start_state.copy() start_col = target_col end_col = target_col + block.shape[1] - if start_col >= first_square_index: - start_col -= first_square_index - end_col -= first_square_index end_row = target_row + block.shape[0] if target_row < self.split_row < end_row: continue @@ -305,7 +349,7 @@ def place_block(self, yield new_state def count_filled_rows(self): - filled = np.nonzero(self.state != 0) + filled = np.nonzero(self.state > self.GAP) if not filled[0].size: used_rows = 0 else: @@ -314,32 +358,9 @@ def count_filled_rows(self): def random_fill(self, shape_counts: typing.Counter[str]): """ Randomly place pieces from shape_counts on empty spaces. """ - assert self.state is not None - 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 - break - else: - next_block = used_blocks[-1] + 1 - if next_block == self.GAP: - next_block += 1 - shape_items = list((shape, count) - for shape, count in shape_counts.items() - if count > 0) - if not shape_items: - return - shuffle(shape_items) - for shape, count in shape_items: - for row, col in empty: - for new_state in self.place_block(shape, row, col, next_block): - shape_counts[shape] -= 1 - self.state = new_state - self.random_fill(shape_counts) - return + self.fill(shape_counts, + are_slots_shuffled=True, + are_partials_saved=True) def flip(self) -> 'BlockPacker': assert self.state is not None @@ -367,7 +388,8 @@ def build_masks(width: int, height: int) -> dict[str, np.ndarray]: :return: {shape_name: mask_array}, where mask_array is a four-dimensional array of occupied spaces with index (start_row, start_col, row, col). In other words, if the shape starts at (start_row, start_col), is - (row, col) filled? + (row, col) filled? (start_row, start_col) is the top-left corner of + the shape, not the first occupied space in the top row. """ all_coordinates = shape_coordinates() all_masks = {} @@ -389,3 +411,17 @@ def build_masks(width: int, height: int) -> dict[str, np.ndarray]: all_masks[name] = masks return all_masks + + +@cache +def get_shape_heights() -> dict[str, int]: + shape_heights = {} + all_coordinates = shape_coordinates() + for shape_name, coordinate_list in all_coordinates.items(): + for rotation, shape in enumerate(coordinate_list): + if len(coordinate_list) == 1: + full_name = shape_name + else: + full_name = f'{shape_name}{rotation}' + shape_heights[full_name] = shape.shape[0] + return shape_heights diff --git a/four_letter_blocks/evo_packer.py b/four_letter_blocks/evo_packer.py index 490c643..bdb56ef 100644 --- a/four_letter_blocks/evo_packer.py +++ b/four_letter_blocks/evo_packer.py @@ -74,7 +74,7 @@ def __repr__(self): def pair(self, other, pair_params): scenario = choices(('mother', 'father', 'mix'), - weights=(5, 5, 1)) + weights=(5, 5, 1))[0] if scenario == 'mother': return Packing(self.value) if scenario == 'father': @@ -292,7 +292,10 @@ def setup(self, pool_count=2) self.shape_counts = shape_counts - def fill(self, shape_counts: typing.Counter[str]) -> bool: + def fill(self, + shape_counts: typing.Counter[str], + are_slots_shuffled: bool = False, + are_partials_saved: bool = False) -> bool: self.setup(shape_counts) while self.current_epoch < self.epochs: if self.run_epoch(): diff --git a/four_letter_blocks/puzzle_pair.py b/four_letter_blocks/puzzle_pair.py index d3bbd5a..c4ad8f1 100644 --- a/four_letter_blocks/puzzle_pair.py +++ b/four_letter_blocks/puzzle_pair.py @@ -55,7 +55,7 @@ def pack_puzzles(self): grid_size, self.block_packer.tries, split_row=self.block_packer.split_row) - is_filled = self.block_packer.fill(self.shape_counts) + is_filled = self.block_packer.fill(Counter(self.shape_counts)) if not is_filled: raise RuntimeError("Blocks didn't fit.") for block in front_puzzle.blocks: diff --git a/four_letter_blocks/puzzle_set.py b/four_letter_blocks/puzzle_set.py index 07df3f6..f1427bd 100644 --- a/four_letter_blocks/puzzle_set.py +++ b/four_letter_blocks/puzzle_set.py @@ -178,7 +178,7 @@ def pack_puzzles(self): self.block_summary = f'{total_block_count} blocks' if extras: self.block_summary += ' with extras: ' + ', '.join(extras) - is_filled = self.block_packer.fill(self.shape_counts) + is_filled = self.block_packer.fill(Counter(self.shape_counts)) if not is_filled: raise RuntimeError("Blocks wouldn't fit.") self.set_face_colours() diff --git a/tests/test_big_puzzle_pair.py b/tests/test_big_puzzle_pair.py index d988978..a73cf1d 100644 --- a/tests/test_big_puzzle_pair.py +++ b/tests/test_big_puzzle_pair.py @@ -81,13 +81,17 @@ def test_draw_front_slug1(pixmap_differ: PixmapDiffer): black_block.face_colour = QColor('black') black_block.border_colour = Block.CUT_COLOUR black_block.tab_count = 2 - black_positions = ((37.5, 37.5), - (112.5, 37.5), - (187.5, 37.5), - (262.5, 62.5), - (12.5, 87.5), - (162.5, 87.5), - (87.5, 112.5)) + black_positions = ((37.5, 12.5), + (137.5, 12.5), + (37.5, 37.5), + (87.5, 37.5), + (187.5, 12.5), + (237.5, 37.5), + (212.5, 112.5), + (237.5, 112.5), + (12.5, 112.5), + (137.5, 87.5), + (87.5, 87.5)) for black_block.x, black_block.y in black_positions: black_block.draw(expected, is_packed=True) @@ -192,24 +196,20 @@ def test_draw_front_slug2(pixmap_differ: PixmapDiffer): black_block.squares[0].size = pair.square_size black_block.face_colour = QColor('black') black_block.tab_count = 2 - black_positions = ((12.5, 162.5), - (187.5, 162.5), - (237.5, 162.5), - (237.5, 187.5), - (12.5, 212.5), - (62.5, 237.5), - (87.5, 237.5), - (137.5, 237.5), - (237.5, 237.5), + black_positions = ((62.5, 137.5), + (212.5, 137.5), + (237.5, 137.5), + (212.5, 187.5), + (12.5, 237.5), + (37.5, 237.5), + (62.5, 162.5), + (112.5, 162.5), + (137.5, 162.5), (262.5, 237.5), (12.5, 262.5), - (87.5, 262.5), - (137.5, 262.5), - (162.5, 262.5), - (187.5, 262.5), - (212.5, 262.5), - (237.5, 262.5), - (262.5, 262.5)) + (262.5, 187.5), + (237.5, 212.5), + (262.5, 212.5)) for black_block.x, black_block.y in black_positions: black_block.draw(expected, is_packed=True) diff --git a/tests/test_block_packer.py b/tests/test_block_packer.py index 39ac93b..9b244f9 100644 --- a/tests/test_block_packer.py +++ b/tests/test_block_packer.py @@ -100,15 +100,29 @@ def test_fill_one_block(): def test_fill_two_blocks(): - width = height = 5 + width, height = 5, 4 shape_counts = Counter('LO') expected_display = dedent("""\ - ##ABB + ..ABB AAABB ..... + .....""") + packer = BlockPacker(width, height) + packer.fill(shape_counts) + + assert packer.display() == expected_display + + +def test_fill_two_blocks_force_fours(): + width, height = 5, 4 + shape_counts = Counter('LO') + expected_display = dedent("""\ + AAABB + A..BB ..... .....""") packer = BlockPacker(width, height) + packer.force_fours = True packer.fill(shape_counts) assert packer.display() == expected_display @@ -134,7 +148,7 @@ def test_fill_three_blocks(): width = height = 5 shape_counts = Counter('OLO') expected_display = dedent("""\ - AABB# + AABB. AABBC ..CCC ..... @@ -149,10 +163,10 @@ def test_no_rotations(): width = height = 5 shape_counts = Counter(('O', 'I0', 'O')) expected_display = dedent("""\ - AABBC - AABBC - ....C - ....C + AABCC + AABCC + ..B.. + ..B.. .....""") packer = BlockPacker(width, height) packer.fill(shape_counts) @@ -164,9 +178,9 @@ def test_no_rotations_needs_gap(): width = height = 5 shape_counts = Counter(('O', 'J3', 'O')) expected_display = dedent("""\ - AA#BB - AACBB - ..CCC + AA#CC + AABCC + ..BBB ..... .....""") packer = BlockPacker(width, height) @@ -306,11 +320,11 @@ def test_fill_with_underhang(): width, height = 3, 5 shape_counts = Counter('OOJ') expected_display = dedent("""\ - AAB - AAB - #BB - CC. - CC.""") + AA. + AA. + BBC + BBC + .CC""") packer = BlockPacker(width, height, tries=100) packer.fill(shape_counts) @@ -321,13 +335,13 @@ def test_fill_with_split_row(): width, height = 3, 7 shape_counts = Counter('OOT') expected_display = dedent("""\ - AA# - AA# - ### - BB# - BBC - .CC - ..C""") + AA. + AA. + ... + BBB + .B. + CC. + CC.""") packer = BlockPacker(width, height, split_row=3, tries=100) packer.fill(shape_counts) @@ -366,6 +380,7 @@ def test_find_slots(): ..#.. ..... .#..#""")) + packer.force_fours = True # Not at (1, 3) or (2, 0), because they cut off something. expected_o_slots = np.array(object=[[0, 1, 0, 0, 0], [1, 0, 0, 0, 0], @@ -387,6 +402,7 @@ def test_find_slots_rotation_allowed(): ..#.. ..... .#..#""")) + packer.force_fours = True # Not at (1, 3) or (2, 0), because they cut off something. expected_s0_slots = np.array(object=[[1, 0, 0, 0, 0], [0, 0, 0, 0, 0], @@ -430,3 +446,24 @@ def test_find_slots_after_fail(): with pytest.raises(RuntimeError, match='Cannot find slots with invalid state.'): packer.find_slots() + + +def test_find_slots_split_row(): + packer = BlockPacker(start_text=dedent("""\ + #..#. + ..... + ..#.. + ..... + .#..#""")) + packer.split_row = 2 + # Not in row 1, because it overlaps the split row. + expected_o_slots = np.array(object=[[0, 1, 0, 0, 0], + [0, 0, 0, 0, 0], + [1, 0, 0, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool) + + o_slots = packer.find_slots()['O'] + + assert str(o_slots) == str(expected_o_slots) diff --git a/tests/test_evo_packer.py b/tests/test_evo_packer.py index 865bb54..190c33d 100644 --- a/tests/test_evo_packer.py +++ b/tests/test_evo_packer.py @@ -64,7 +64,9 @@ def test_mutate(): @patch('four_letter_blocks.evo_packer.randrange') -def test_pair(mock_randrange): +@patch('four_letter_blocks.evo_packer.choices') +def test_pair(mock_choices, mock_randrange): + mock_choices.side_effect = [['mix']] mock_randrange.side_effect = [0, 0, 4, 4] shape_counts1 = Counter({'I0': 4}) @@ -104,7 +106,9 @@ def test_pair(mock_randrange): @patch('four_letter_blocks.evo_packer.randrange') -def test_pair_with_fill(mock_randrange): +@patch('four_letter_blocks.evo_packer.choices') +def test_pair_with_fill(mock_choices, mock_randrange): + mock_choices.side_effect = [['mix']] mock_randrange.side_effect = [0, 0, 4, 4] shape_counts1 = Counter({'L1': 1}) diff --git a/tests/test_puzzle_pair.py b/tests/test_puzzle_pair.py index 54d0530..a65985d 100644 --- a/tests/test_puzzle_pair.py +++ b/tests/test_puzzle_pair.py @@ -90,14 +90,14 @@ def test_draw_blocks(pixmap_differ: PixmapDiffer): black = QColor('black') draw_gradient_rect(expected, black, 51.25, 31.25, 17.5, 17.5, 6.25) - draw_gradient_rect(expected, black, 91.25, 31.25, 17.5, 17.5, 6.25) + draw_gradient_rect(expected, black, 11.25, 31.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 91.25, 51.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 11.25, 51.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 11.25, 91.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 211.25, 91.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 211.25, 51.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 131.25, 51.25, 17.5, 17.5, 6.25) - draw_gradient_rect(expected, black, 131.25, 31.25, 17.5, 17.5, 6.25) + draw_gradient_rect(expected, black, 211.25, 31.25, 17.5, 17.5, 6.25) draw_gradient_rect(expected, black, 171.25, 31.25, 17.5, 17.5, 6.25) front.face_colour = QColor('transparent') back.face_colour = QColor('transparent') @@ -105,16 +105,16 @@ def test_draw_blocks(pixmap_differ: PixmapDiffer): blocks = front.blocks blocks[0].set_display(70, 70, 0) blocks[1].set_display(30, 50, 0) - blocks[2].set_display(10, 10, 0) - blocks[3].set_display(50, 10, 0) + blocks[2].set_display(70, 10, 0) + blocks[3].set_display(10, 10, 0) blocks[4].set_display(10, 70, 0) blocks = back.blocks blocks[0].set_display(130, 70, 0) blocks[1].set_display(170, 70, 0) - blocks[2].set_display(190, 10, 0) + blocks[2].set_display(130, 10, 0) blocks[3].set_display(150, 50, 0) - blocks[4].set_display(130, 10, 0) + blocks[4].set_display(170, 10, 0) for block in front.blocks + back.blocks: block.draw(expected, is_packed=True) @@ -306,8 +306,8 @@ def test_draw_front(pixmap_differ: PixmapDiffer): def test_draw_cuts(pixmap_differ: PixmapDiffer): block_text = dedent("""\ - AABBB - AA#B# + AAABB + #A#BB #CCC# DDCEE #DDEE @@ -340,7 +340,7 @@ def test_draw_cuts(pixmap_differ: PixmapDiffer): block.squares[0].size = puzzle.square_size block.tab_count = 2 block.border_colour = block.CUT_COLOUR - for block.x, block.y in ((120, 30), (0, 60), (120, 60), (0, 120)): + for block.x, block.y in ((0, 30), (0, 60), (120, 60), (0, 120)): block.draw_outline(expected) pair2 = parse_puzzle_pair() @@ -369,9 +369,9 @@ def test_draw_back(pixmap_differ: PixmapDiffer): def test_packing(): expected_packing = dedent("""\ - AABBB - AA#B# - #CCC# + AAABB + .A.BB + #CCC. DDCEE .DDEE""") puzzle_pair = parse_puzzle_pair(BlockPacker(5, 5, tries=1000)) @@ -424,9 +424,9 @@ def test_prepacking_useless(): CCCC DDDD""") expected_packing = dedent("""\ - AABBB - AA#B# - #CCC# + AAABB + .A.BB + #CCC. DDCEE .DDEE""") puzzle_pair = parse_puzzle_pair(BlockPacker(start_text=start_text, diff --git a/tests/test_puzzle_set.py b/tests/test_puzzle_set.py index 729f07a..dede4c6 100644 --- a/tests/test_puzzle_set.py +++ b/tests/test_puzzle_set.py @@ -305,17 +305,17 @@ 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, 10, 2) - blocks[2].set_display(50, 10, 1) + 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(50, 70, 3) + blocks[4].set_display(30, 70, 0) blocks = puzzle2.blocks - blocks[0].set_display(90, 50, 1) - blocks[1].set_display(130, 30, 0) - blocks[2].set_display(230, 30, 1) - blocks[3].set_display(210, 70, 1) - blocks[4].set_display(210, 10, 3) + blocks[0].set_display(70, 50, 0) + blocks[1].set_display(70, 90, 1) + blocks[2].set_display(170, 10, 0) + blocks[3].set_display(250, 70, 0) + blocks[4].set_display(230, 10, 0) for block in puzzle1.blocks + puzzle2.blocks: block.draw(expected, is_packed=True) @@ -334,11 +334,11 @@ def test_draw_cuts(pixmap_differ: PixmapDiffer): 180, 'test_puzzle_draw_cuts') as (actual, expected): block_text = dedent("""\ - #ABBBCC - #AD#BCE - AADDFCE - GGHDFFE - GGHHHFE + #ABCCCC + #ABBBDD + AAEEDDE + F#EEGGE + FFFGGEE """) puzzle3 = Puzzle.parse_sections('', block_text, @@ -351,7 +351,7 @@ def test_draw_cuts(pixmap_differ: PixmapDiffer): block.border_colour = Block.CUT_COLOUR block.draw_outline(expected) - puzzle_set = parse_puzzle_set(BlockPacker(7, 8, tries=1000)) + puzzle_set = parse_puzzle_set(BlockPacker(7, 8, tries=4000)) puzzle_set.square_size = 20 puzzle_set.draw_cuts(actual)