diff --git a/four_letter_blocks/block.py b/four_letter_blocks/block.py index 1e06ae2..6ad0b8f 100644 --- a/four_letter_blocks/block.py +++ b/four_letter_blocks/block.py @@ -110,6 +110,15 @@ def shape_rotation_names() -> typing.List[str]: names.sort() return names + @property + def rotated_shape(self) -> str: + shape = self.shape + assert shape is not None + + if shape != 'O': + shape += str(self.shape_rotation) + return shape + @property def face_colour(self): return self.squares[0].face_colour diff --git a/four_letter_blocks/block_packer.py b/four_letter_blocks/block_packer.py index 6bcdc7f..a975767 100644 --- a/four_letter_blocks/block_packer.py +++ b/four_letter_blocks/block_packer.py @@ -202,11 +202,15 @@ def display(self, state: np.ndarray | None = None) -> str: for c in row) for row in state) - def sort_blocks(self): + def sort_blocks(self, + block_nums: typing.Sequence[int] | None = None) -> None: + if block_nums is None: + block_nums = range(2, 256) + block_nums_iter = iter(block_nums) + assert self.state is not None state = np.zeros(self.state.shape, np.uint8) gap_spaces = self.state == 1 state += gap_spaces - next_block = 2 for row in range(state.shape[0]): for col in range(state.shape[1]): new_block = state[row, col] @@ -219,8 +223,8 @@ def sort_blocks(self): continue # noinspection PyUnresolvedReferences block_spaces = (self.state == old_block).astype(np.uint8) + next_block = next(block_nums_iter) state += next_block * block_spaces - next_block += 1 self.state = state def create_blocks(self) -> typing.Iterable[Block]: @@ -257,7 +261,7 @@ 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] | None = None) -> bool: """ Fill in the current state with the given shapes. Cycles through the available shapes in shape_counts, and tries them in @@ -273,7 +277,8 @@ 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. Adjusted - to remaining counts, if self.are_partials_saved is True. + to remaining counts, if self.are_partials_saved is True. If None, + then calls calculate_max_shape_counts(). :return: True, if all requested shapes have been placed, or if no gaps are left, otherwise False. """ @@ -287,6 +292,8 @@ def fill(self, shape_counts: typing.Counter[str]) -> bool: best_state = None assert self.state is not None start_state = self.state + if shape_counts is None: + shape_counts = self.calculate_max_shape_counts() if not sum(shape_counts.values()): # Nothing to add! best_state = start_state @@ -453,6 +460,25 @@ def place_block(self, target += block_num * block yield new_state + def remove_block(self, row: int, col: int) -> str: + """ Remove a block from the current state. + + :param row: row to remove the block from + :param col: column to remove the block from + :return: the shape of the removed block + :raises ValueError: if there is no block at that position""" + assert self.state is not None + block_num = self.state[row, col] + if block_num <= 1: + raise ValueError(f'No block at ({row}, {col}).') + block = self.create_block(block_num) + shape = block.shape + if shape != 'O': + shape += str(block.shape_rotation) + self.state[self.state == block_num] = 0 + + return shape + def count_filled_rows(self): filled = np.nonzero(self.state > self.GAP) if not filled[0].size: diff --git a/four_letter_blocks/double_block_packer.py b/four_letter_blocks/double_block_packer.py index b66cc05..b1cd3d3 100644 --- a/four_letter_blocks/double_block_packer.py +++ b/four_letter_blocks/double_block_packer.py @@ -1,5 +1,5 @@ import typing -from collections import Counter +from collections import Counter, defaultdict import numpy as np @@ -8,21 +8,27 @@ class DoubleBlockPacker: - def __init__(self, front_text: str, back_text: str, tries: int) -> None: - front_lines = front_text.splitlines() - width = len(front_lines[0]) - height = len(front_lines) - - self.front_packer = BlockPacker(width, - height, - start_text=front_text) + def __init__(self, + front_text: str | None = None, + back_text: str | None = None, + tries: int = -1, + start_state: np.ndarray | None = None) -> None: + if start_state is None: + front_state = back_state = None + else: + full_height = start_state.shape[0] + front_state = start_state[:full_height//2] + back_state = start_state[full_height//2:] + self.front_packer = BlockPacker(start_text=front_text, + start_state=front_state) self.front_packer.force_fours = True + self.width = self.front_packer.width + self.height = self.front_packer.height front_unused = np.count_nonzero( self.front_packer.state == BlockPacker.UNUSED) - self.back_packer = BlockPacker(width, - height, - start_text=back_text) + self.back_packer = BlockPacker(start_text=back_text, + start_state=back_state) self.back_packer.force_fours = True back_unused = np.count_nonzero( self.back_packer.state == BlockPacker.UNUSED) @@ -33,17 +39,27 @@ def __init__(self, front_text: str, back_text: str, tries: int) -> None: self.front_shape_counts = self.front_packer.calculate_max_shape_counts() self.tries = tries self.is_full = False + self.are_slots_shuffled = False self.needed_block_count = front_unused // 4 - def fill(self) -> bool: + @property + def state(self): + return np.concatenate((self.front_packer.state, self.back_packer.state)) + + def fill(self, shape_counts: dict[str, np.ndarray] | None = None) -> bool: """ Fill both front and back with the same block shapes and rotations. :return: True if no gaps remain, False otherwise. """ + if shape_counts is None: + front_shape_counts = self.front_shape_counts + else: + front_shape_counts = shape_counts if self.tries == 0: # print('0 tries left.') return False - self.tries -= 1 + if self.tries > 0: + self.tries -= 1 width = self.front_packer.width height = self.front_packer.height flipped_shape_names = flipped_shapes() @@ -87,10 +103,10 @@ def fill(self) -> bool: shape_scores: typing.Counter[str] = Counter() for shape, slot_count in slot_counts.items(): if is_front_first: - target_count = self.front_shape_counts[shape] + target_count = front_shape_counts[shape] else: front_shape = flipped_shape_names[shape] - target_count = self.front_shape_counts[front_shape] + target_count = front_shape_counts[front_shape] if target_count == 0: continue # noinspection PyTypeChecker @@ -101,6 +117,10 @@ def fill(self) -> bool: start_state2 = packer2.state assert start_state2 is not None next_block = packer1.find_next_block() + if self.are_slots_shuffled: + rng = np.random.default_rng() + else: + rng = None for shape1, _score in shape_scores.most_common(): shape2 = flipped_shape_names[shape1] masks1 = all_masks[shape1] @@ -112,6 +132,8 @@ def fill(self) -> bool: mins1) if all_coords1.size == 0: continue + if rng is not None: + rng.shuffle(all_coords1) slots2_masked = masks2[shape2_slots].any(axis=0)[:width, :height] slots2_coverage = slots2_masked * coverage2 uncovered2 = slots2_coverage == 0 @@ -156,7 +178,7 @@ def fill(self) -> bool: # f'index1 ({slot_row1}, {slot_col1}), ' # f'index2 ({slot_row2}, {slot_col2})') # print(self.display()) - self.is_full = self.fill() + self.is_full = self.fill(front_shape_counts) if self.is_full: # print('Full!') return True @@ -168,6 +190,20 @@ def fill(self) -> bool: # print('Tried all minimum slots.') return False + def remove_block(self, row: int, col: int) -> str: + """ Remove a block from the current state. + + :param row: row to remove the block from + :param col: column to remove the block from + :return: the shape of the removed block + :raises ValueError: if there is no block at that position""" + block_num = self.state[row, col] + shape = self.front_packer.remove_block(row, col) + back_block = self.back_packer.create_block(block_num) + back_square = back_block.squares[0] + self.back_packer.remove_block(back_square.y, back_square.x) + return shape + @staticmethod def find_slot_coords(shape_slots, masks, min_coverages): all_coords = np.ndarray((0, 2), dtype=int) @@ -183,6 +219,20 @@ def sort_blocks(self): self.front_packer.sort_blocks() self.back_packer.sort_blocks() + front_blocks = defaultdict(list) + for block_num, block in self.front_packer.create_blocks_with_block_num(): + shape = block.rotated_shape + front_blocks[shape].append(block_num) + + flipped_shape_names = flipped_shapes() + block_nums = [] + for block_num, block in self.back_packer.create_blocks_with_block_num(): + back_shape = block.rotated_shape + front_shape = flipped_shape_names[back_shape] + back_block_num = front_blocks[front_shape].pop(0) + block_nums.append(back_block_num) + self.back_packer.sort_blocks(block_nums) + def display(self) -> str: front_display = self.front_packer.display() back_display = self.back_packer.display() diff --git a/four_letter_blocks/double_evo_packer.py b/four_letter_blocks/double_evo_packer.py new file mode 100644 index 0000000..ed24641 --- /dev/null +++ b/four_letter_blocks/double_evo_packer.py @@ -0,0 +1,55 @@ +import typing + +import numpy as np + +from four_letter_blocks.double_block_packer import DoubleBlockPacker +from four_letter_blocks.evo_packer import (EvoPacker, PackingFitnessCalculator, + FitnessScore) + + +class DoubleEvoPacker(EvoPacker): + def __init__(self, + front_text: str, + back_text: str, + tries: int = -1, + start_state: np.ndarray | None = None) -> None: + start_packer = DoubleBlockPacker(front_text, + back_text, + tries, + start_state) + super().__init__(start_state=start_packer.state[0:start_packer.height], + tries=tries) + self.state = start_packer.state + self.front_shape_counts = start_packer.front_shape_counts + + def setup(self, + shape_counts: typing.Counter[str] | None = None, + fitness_calculator: PackingFitnessCalculator | None = None) -> None: + if fitness_calculator is None: + fitness_calculator = DoublePackingFitnessCalculator() + if shape_counts is None: + shape_counts = self.front_shape_counts.copy() + super().setup(shape_counts, fitness_calculator) + + def create_init_params(self, shape_counts): + init_params = super().create_init_params(shape_counts) + init_params['packer_class'] = DoubleBlockPacker + return init_params + + def display(self, state: np.ndarray | None = None) -> str: + if state is None: + state = self.state + packer = DoubleBlockPacker(start_state=state) + return packer.display() + + +class DoublePackingFitnessCalculator(PackingFitnessCalculator): + def calculate_from_state(self, state) -> FitnessScore: + full_height = state.shape[0] + height = full_height // 2 + front_state = state[:height] + back_state = state[height:] + front_score = super().calculate_from_state(front_state) + back_score = super().calculate_from_state(back_state) + front_score.warning_count += back_score.warning_count + return front_score diff --git a/four_letter_blocks/evo_packer.py b/four_letter_blocks/evo_packer.py index c0308e1..c2261ae 100644 --- a/four_letter_blocks/evo_packer.py +++ b/four_letter_blocks/evo_packer.py @@ -1,6 +1,8 @@ import typing from collections import Counter +from copy import deepcopy from dataclasses import dataclass +from datetime import datetime from functools import cache from itertools import count from random import randrange, choices, choice @@ -71,12 +73,18 @@ def move(self, row: int, col: int): class Packing(Individual): + """ Represents one individual in an evolutionary population of packings. + + init_params control the packing, see _random_init() for details. + self.value records the state and controls the packing, see the return value + of _random_init() for details. + """ def __repr__(self): return f'Packing({self.value!r})' def pair(self, other, pair_params): scenario = choices(('mother', 'father', 'mix'), - weights=(5, 5, 1))[0] + weights=(5, 5, 1*0))[0] if scenario == 'mother': return Packing(self.value) if scenario == 'father': @@ -100,8 +108,10 @@ def pair(self, other, pair_params): for (i1, j1), (i2, j2) in zip(positions1, positions2): mover1.move(i1, j1) mover2.move(i2, j2) - packer = BlockPacker(start_state=new_state) - packer.force_fours = self.value.get('force_fours', False) + packer_class = self.value['packer_class'] + tries = self.value['tries'] + packer = packer_class(start_state=new_state, tries=tries) + packer.force_fours = self.value['force_fours'] packer.are_slots_shuffled = True packer.are_partials_saved = True packer.fill(mover1.shape_counts) @@ -111,46 +121,50 @@ def pair(self, other, pair_params): shape_counts=mover1.shape_counts, can_rotate=can_rotate, pos1=(row1, col1), - pos2=(row2, col2))) + pos2=(row2, col2), + packer_class=packer_class, + force_fours=packer.force_fours, + tries=tries)) def mutate(self, mutate_params) -> None: self.value: dict - state: np.ndarray = self.value['state'].copy() + state: np.ndarray | tuple = deepcopy(self.value['state']) shape_counts = Counter(self.value['shape_counts']) can_rotate: bool = self.value['can_rotate'] - block_packer = BlockPacker(start_state=state) - block_packer.force_fours = self.value.get('force_fours', False) + packer_class = self.value['packer_class'] + tries = self.value['tries'] + block_packer: BlockPacker = packer_class(start_state=state, + tries=tries) + block_packer.force_fours = self.value['force_fours'] block_packer.are_partials_saved = True block_packer.are_slots_shuffled = True - grid_size = state.shape[0] - gaps = np.argwhere(state == 0) + start_state = block_packer.state + grid_size = max(block_packer.width, block_packer.height) + gaps = np.argwhere(start_state == 0) if gaps.size > 0: row0, col0 = choice(gaps) else: - row0 = randrange(grid_size) - col0 = randrange(grid_size) - block_count = (state > 1).sum() // 4 + row0 = randrange(block_packer.height) + col0 = randrange(block_packer.width) + block_count = (start_state > 1).sum() // 4 # type: ignore min_removed = 0 # min(3, block_count) max_removed = min(10, block_count) remove_count = randrange(min_removed, max_removed+1) positions = ranked_offsets(grid_size) + [row0, col0] for row, col in positions[1:]: - if not 0 <= row < grid_size: + if not 0 <= row < block_packer.height: continue - if not 0 <= col < grid_size: + if not 0 <= col < block_packer.width: continue - block_num = state[row, col] - if block_num <= 1: + try: + shape = block_packer.remove_block(row, col) + except ValueError: continue - block = block_packer.create_block(block_num) - shape = block.shape - if not can_rotate: - shape += str(block.shape_rotation) + shape_counts[shape] += 1 - state[state == block_num] = 0 remove_count -= 1 if remove_count == 0: break @@ -160,14 +174,20 @@ def mutate(self, mutate_params) -> None: assert block_packer.state is not None self.value = dict(state=block_packer.state, shape_counts=shape_counts, - can_rotate=can_rotate) + can_rotate=can_rotate, + packer_class=packer_class, + force_fours=block_packer.force_fours, + tries=tries) def _random_init(self, init_params: dict): start_state = init_params['start_state'] shape_counts = Counter(init_params['shape_counts']) can_rotate = all(len(shape) == 1 for shape in shape_counts) - block_packer = BlockPacker(start_state=start_state) - block_packer.force_fours = True + packer_class = init_params.get('packer_class', BlockPacker) + tries = init_params['tries'] + block_packer = packer_class(start_state=start_state, + tries=tries) + block_packer.force_fours = init_params.get('force_fours', False) block_packer.are_slots_shuffled = True block_packer.are_partials_saved = True block_packer.fill(shape_counts) @@ -175,7 +195,10 @@ def _random_init(self, init_params: dict): assert block_packer.state is not None return dict(state=block_packer.state, shape_counts=shape_counts, - can_rotate=can_rotate) + can_rotate=can_rotate, + force_fours=block_packer.force_fours, + packer_class=packer_class, + tries=tries) @dataclass(order=True) @@ -205,16 +228,21 @@ def format_details(self): self.details.clear() return display - def calculate(self, packing): - """ Calculate fitness score based on the solution. - - -1 for every unused block in shape_counts. - """ + def calculate(self, packing: Packing) -> FitnessScore: + """ Calculate fitness score based on the solution. """ value = packing.value - fitness = value.get('fitness') - if fitness is not None: - return fitness + fitness_x: FitnessScore | None = value.get('fitness') + if fitness_x is not None: + return fitness_x state = value['state'] + fitness = self.calculate_from_state(state) + self.summaries.append(str(fitness)) + + value['fitness'] = fitness + return fitness + + def calculate_from_state(self, state) -> FitnessScore: + # noinspection PyTypeChecker empty = np.nonzero(state == 0) empty_spaces = empty[0].size missed_targets = 0 @@ -235,7 +263,10 @@ def calculate(self, packing): for shape, parity in self.count_parities.items(): if shape_counts[shape] % 2 != parity: missed_targets += 1 - for (shape1, shape2), expected_diff in self.count_diffs.items(): + for shapes, expected_diff in self.count_diffs.items(): + assert len(shapes) == 2 + shape1 = shapes[0] + shape2 = shapes[1] actual_diff = shape_counts[shape1] - shape_counts[shape2] missed_targets += abs(actual_diff) for shapes, expected_min in self.count_min.items(): @@ -260,8 +291,6 @@ def calculate(self, packing): missed_targets=-missed_targets, warning_count=-warning_count) self.summaries.append(str(fitness)) - - value['fitness'] = fitness return fitness @@ -285,15 +314,17 @@ def __init__(self, self.current_epoch = 0 self.shape_counts: typing.Counter[str] = Counter() self.evo: Evolution | None = None - self.top_fitness: FitnessScore = FitnessScore(0, 0) + self.top_fitness: FitnessScore = FitnessScore( + -self.width * self.height, + 0) self.top_blocks = '' + self.top_choices: set[str] = set() def setup(self, shape_counts: typing.Counter[str], fitness_calculator: PackingFitnessCalculator | None = None): assert self.state is not None - init_params = dict(start_state=self.state.copy(), - shape_counts=shape_counts) + init_params = self.create_init_params(shape_counts) if fitness_calculator is None: fitness_calculator = PackingFitnessCalculator() fitness_calculator.summaries.clear() @@ -309,10 +340,15 @@ def setup(self, pool_count=2) self.shape_counts = shape_counts - def fill(self, - shape_counts: typing.Counter[str], - are_slots_shuffled: bool = False, - are_partials_saved: bool = False) -> bool: + def create_init_params(self, shape_counts): + init_params = dict(start_state=self.state.copy(), + shape_counts=shape_counts, + tries=self.tries) + return init_params + + def fill(self, shape_counts: typing.Counter[str] | None = None) -> bool: + if shape_counts is None: + shape_counts = self.calculate_max_shape_counts() self.setup(shape_counts) while self.current_epoch < self.epochs: if self.run_epoch(): @@ -336,16 +372,27 @@ def run_epoch(self) -> bool: pool_fitness = pool.fitness(pool.individuals[-1]) summaries.append(f'{pool_fitness}') if self.is_logging: - print(self.current_epoch, + print(datetime.now().strftime('%H:%M'), + self.current_epoch, top_fitness, mid_fitness, ', '.join(summaries)) - print(self.top_blocks) - self.top_fitness = top_fitness - packer = BlockPacker(start_state=top_individual.value['state']) - packer.force_fours = True - packer.sort_blocks() - self.top_blocks = packer.display() + if top_fitness >= self.top_fitness: + packer_class = top_individual.value['packer_class'] + packer = packer_class(start_state=top_individual.value['state'], + tries=top_individual.value['tries']) + packer.force_fours = True + packer.sort_blocks() + packer_display = packer.display() + if top_fitness > self.top_fitness: + self.top_fitness = top_fitness + self.top_choices.clear() + self.top_blocks = packer_display + if packer_display not in self.top_choices: + self.top_choices.add(packer_display) + if self.is_logging: + print(f'Packing {len(self.top_choices)}:') + print(packer_display) if (top_fitness.empty_spaces == 0 and top_fitness.missed_targets == 0 and top_fitness.warning_count == 0): diff --git a/tests/test_block_packer.py b/tests/test_block_packer.py index 803d6ba..6bda8df 100644 --- a/tests/test_block_packer.py +++ b/tests/test_block_packer.py @@ -1,3 +1,4 @@ +import re from collections import Counter from textwrap import dedent @@ -414,6 +415,67 @@ def test_fill_fail(): assert not is_filled +# noinspection DuplicatedCode +def test_place_block(): + packer = BlockPacker(start_text=dedent("""\ + #..#. + ..... + AA#.. + AA... + .#..#""")) + expected_display = dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..#""") + + states = list(packer.place_block('L0', + 1, + 3, + 3)) + + assert len(states) == 1 + assert packer.display(states[0]) == expected_display + + +# noinspection DuplicatedCode +def test_remove_block(): + packer = BlockPacker(start_text=dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..#""")) + expected_display = dedent("""\ + #..#. + ..... + AA#.. + AA... + .#..#""") + + shape = packer.remove_block(1, 3) + + assert shape == 'L0' + assert packer.display() == expected_display + + +# noinspection DuplicatedCode +def test_remove_block_misses(): + expected_display = dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..#""") + packer = BlockPacker(start_text=expected_display) + + with pytest.raises(ValueError, match=re.escape('No block at (1, 2).')): + packer.remove_block(1, 2) + + assert packer.display() == expected_display + + # noinspection DuplicatedCode def test_find_slots(): packer = BlockPacker(start_text=dedent("""\ diff --git a/tests/test_double_block_packer.py b/tests/test_double_block_packer.py index 1588730..1401aa0 100644 --- a/tests/test_double_block_packer.py +++ b/tests/test_double_block_packer.py @@ -1,5 +1,6 @@ from textwrap import dedent +import numpy as np import pytest from four_letter_blocks.double_block_packer import DoubleBlockPacker @@ -27,6 +28,48 @@ def test_different_space_count(): def test_fill(): + expected_display = dedent("""\ + #ABBBB# + AAA#CCD + EFFFCCD + E#F#G#D + EHHHGID + EHJ#GII + #JJJGI# + + HHHFFF# + D#H#FCC + DBBBBCC + D#E#A#G + DIEAAAG + IIE#J#G + #IEJJJG""") + front_text = dedent("""\ + #.BBBB# + ...#..D + ......D + .#.#.#D + ......D + ...#... + #.....#""") + back_text = dedent("""\ + ......# + D#.#... + DBBBB.. + D#.#.#. + D...... + ...#.#. + #......""") + packer = DoubleBlockPacker(front_text, back_text, tries=40_000) + packer.fill() + + assert packer.is_full + packer.sort_blocks() + + assert packer.display() == expected_display + + +def test_slots_shuffled(): front_text = dedent("""\ #?????# ???#??? @@ -43,11 +86,152 @@ def test_fill(): ??????? ???#?#? #??????""") + displays = set() + + for _ in range(4): + packer = DoubleBlockPacker(front_text, back_text, tries=400) + packer.are_slots_shuffled = True + + packer.fill() + + assert packer.is_full + packer.sort_blocks() + displays.add(packer.display()) + + assert len(displays) > 1 + for display in displays: + print('---') + print(display) + + +def test_state(): + front_text = dedent("""\ + AAA + A#B + BBB""") + back_text = dedent("""\ + AAB + A#B + ABB""") + expected_state = np.array([[2, 2, 2], + [2, 1, 3], + [3, 3, 3], + [2, 2, 3], + [2, 1, 3], + [2, 3, 3]]) + packer = DoubleBlockPacker(front_text, back_text, tries=400) - packer.fill() - assert packer.is_full - # packer.sort_blocks() - # print(packer.display()) - # print(f'{packer.tries} tries left.') - # assert False + double_state = packer.state + + np.testing.assert_array_equal(double_state, expected_state) + + +# noinspection DuplicatedCode +def test_remove_block(): + packer = DoubleBlockPacker( + front_text=dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..#"""), + back_text=dedent("""\ + #AA.# + .AA.. + ..#B. + ...B. + #.BB#""")) + expected_display = dedent("""\ + #..#. + ..... + AA#.. + AA... + .#..# + + #AA.# + .AA.. + ..#.. + ..... + #...#""") + + shape = packer.remove_block(1, 3) + + assert shape == 'L0' + assert packer.display() == expected_display + + +# noinspection DuplicatedCode +def test_start_state(): + packer1 = DoubleBlockPacker( + front_text=dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..#"""), + back_text=dedent("""\ + #AA.# + .AA.. + ..#B. + ...B. + #.BB#""")) + expected_display = dedent("""\ + #..#. + ...B. + AA#B. + AA.BB + .#..# + + #AA.# + .AA.. + ..#B. + ...B. + #.BB#""") + + start_state = packer1.state.copy() + packer2 = DoubleBlockPacker(start_state=start_state) + + assert packer2.display() == expected_display + + +def test_sort_blocks(): + front_text = dedent("""\ + #AAAAB# + CDD#BBE + CDDFFBE + C#G#F#E + CGGGFHE + III#HHH + #IJJJJ#""") + back_text = dedent("""\ + AAAABB# + C#D#BEE + CDDDBEE + C#F#G#H + CFFFGGH + III#G#H + #IJJJJH""") + + expected_display = dedent("""\ + #AAAAB# + CDD#BBE + CDDFFBE + C#G#F#E + CGGGFHE + III#HHH + #IJJJJ# + + AAAAFF# + C#G#FDD + CGGGFDD + C#H#B#E + CHHHBBE + III#B#E + #IJJJJE""") + + packer = DoubleBlockPacker(front_text, back_text) + + packer.sort_blocks() + + assert packer.display() == expected_display diff --git a/tests/test_double_evo_packer.py b/tests/test_double_evo_packer.py new file mode 100644 index 0000000..33cb6da --- /dev/null +++ b/tests/test_double_evo_packer.py @@ -0,0 +1,122 @@ +from collections import Counter +from textwrap import dedent +from unittest.mock import patch + +import pytest + +from four_letter_blocks.double_block_packer import DoubleBlockPacker +from four_letter_blocks.double_evo_packer import DoubleEvoPacker +from four_letter_blocks.evo_packer import Packing + + +@pytest.mark.skip(reason="Too slow for a unit test.") +def test_double_evo_packer(): + front_text = dedent("""\ + ???#????? + ???#????? + ???#????? + ??????### + ????#???? + ###?????? + ?????#??? + ?????#??? + ?????#???""") + back_text = dedent("""\ + ???#????? + ???#????? + ???#????? + ????????? + ###?#?### + ????????? + ?????#??? + ?????#??? + ?????#???""") + packer = DoubleEvoPacker(front_text, back_text, tries=5000) + packer.force_fours = True + packer.epochs = 50 + packer.pool_size = 100 + packer.is_logging = True + packer.fill() + + assert packer.is_full + packer.sort_blocks() + print('---') + print(packer.display()) + print(f'{packer.tries} tries left.') + assert False + + +@patch('four_letter_blocks.evo_packer.randrange') +@patch('four_letter_blocks.evo_packer.choices') +@pytest.mark.skip(reason="not implemented yet, only mutating") +def test_pair(mock_choices, mock_randrange): + mock_choices.side_effect = [['mix']] + mock_randrange.side_effect = [0, 0, 6, 6] + + shape_counts1 = Counter() + front_text1 = dedent("""\ + #AAAAB# + CDD#BBE + CDDFFBE + C#G#F#E + CGGGFHE + III#HHH + #IJJJJ#""") + back_text1 = dedent("""\ + AAAAFF# + C#G#FDD + CGGGFDD + C#H#B#E + CHHHBBE + III#B#E + #IJJJJE""") + front_text2 = dedent("""\ + #ABBBB# + AAA#CCD + EFFFCCD + E#F#G#D + EHGGGID + EHH#III + #HJJJJ#""") + back_text2 = dedent("""\ + DBBBBA# + D#H#AAA + DHHFFFE + D#H#F#E + CCJJJJE + CCI#G#E + #IIIGGG""") + expected_display = dedent("""\ + #AAAA.# + C..#... + C...... + C#.#.#. + C...... + ...#... + #.JJJJ# + + AAAA..# + C#.#... + C...... + C#.#.#. + C.JJJJ. + ...#.#. + #...... + """) + packer1 = DoubleEvoPacker(front_text=front_text1, back_text=back_text1) + packing1 = Packing(dict(state=packer1.state, + shape_counts=shape_counts1, + can_rotate=False, + packer_class=DoubleBlockPacker)) + shape_counts2 = Counter() + packer2 = DoubleEvoPacker(front_text=front_text2, back_text=back_text2) + packing2 = Packing(dict(state=packer2.state, + shape_counts=shape_counts2, + can_rotate=False, + packer_class=DoubleBlockPacker)) + expected_shape_counts = {'I0': 2, 'O': 2} + + child = packing1.pair(packing2, {}) + + assert packer2.display(child.value['state']) == expected_display + assert child.value['shape_counts'] == expected_shape_counts diff --git a/tests/test_evo_packer.py b/tests/test_evo_packer.py index 7727cac..4c5e794 100644 --- a/tests/test_evo_packer.py +++ b/tests/test_evo_packer.py @@ -4,6 +4,7 @@ import numpy as np +from four_letter_blocks.block_packer import BlockPacker from four_letter_blocks.evo_packer import EvoPacker, Packing,\ PackingFitnessCalculator, FitnessScore, distance_ranking, ranked_offsets @@ -49,7 +50,10 @@ def test_mutate(): start_state = packer.state packing = Packing(dict(state=start_state, shape_counts=shape_counts, - can_rotate=True)) + can_rotate=True, + force_fours=False, + packer_class=BlockPacker, + tries=100)) mutate_params = None packing.mutate(mutate_params) @@ -81,7 +85,10 @@ def test_pair(mock_choices, mock_randrange): packer1 = EvoPacker(start_text=start_text1) packing1 = Packing(dict(state=packer1.state, shape_counts=shape_counts1, - can_rotate=False)) + can_rotate=False, + force_fours=False, + packer_class=BlockPacker, + tries=100)) shape_counts2 = Counter({'O': 4}) start_text2 = dedent("""\ D##B. @@ -92,7 +99,10 @@ def test_pair(mock_choices, mock_randrange): packer2 = EvoPacker(start_text=start_text2) packing2 = Packing(dict(state=packer2.state, shape_counts=shape_counts2, - can_rotate=False)) + can_rotate=False, + force_fours=False, + packer_class=BlockPacker, + tries=100)) expected_display = dedent("""\ .##B. CC.BA @@ -123,7 +133,10 @@ def test_pair_with_fill(mock_choices, mock_randrange): packer1 = EvoPacker(start_text=start_text1) packing1 = Packing(dict(state=packer1.state, shape_counts=shape_counts1, - can_rotate=False)) + can_rotate=False, + force_fours=False, + packer_class=BlockPacker, + tries=100)) shape_counts2 = Counter({'O': 1, 'J1': 1}) start_text2 = dedent("""\ .##B. @@ -134,7 +147,10 @@ def test_pair_with_fill(mock_choices, mock_randrange): packer2 = EvoPacker(start_text=start_text2) packing2 = Packing(dict(state=packer2.state, shape_counts=shape_counts2, - can_rotate=False)) + can_rotate=False, + force_fours=False, + packer_class=BlockPacker, + tries=100)) expected_display = dedent("""\ .##.D AADDD